diff --git a/Credits.txt b/Credits.txt
index 76e2f76c..4d366122 100644
--- a/Credits.txt
+++ b/Credits.txt
@@ -6,7 +6,7 @@ has been used throughout the IPS Community Suite to add style, flair and functio
to our product.
NULLING
- Last Updated: 15 March 2017
+ Last Updated: 04 May 2017
Author: IPBMafia.ru
AES implementation in PHP
diff --git a/admin/convertutf8/system/Output/Browser/Template.php b/admin/convertutf8/system/Output/Browser/Template.php
index 361dc2d3..ada99808 100644
--- a/admin/convertutf8/system/Output/Browser/Template.php
+++ b/admin/convertutf8/system/Output/Browser/Template.php
@@ -23,7 +23,7 @@ class Template
$url = \IPSUtf8\Output\Browser::$url;
$title = ( empty( $title ) ) ? 'IPS Converter' : $title;
$active = array( 'convert' => '', 'tools' => '', 'archive' => '' );
- $hasArchive = \IPSUtf8\Session::i()->has_archive;
+ $controller = preg_replace( "/[^a-zA-Z0-9]/", '', empty( \IPSUtf8\Request::i()->controller ) ? 'browser' : \IPSUtf8\Request::i()->controller );
$controller = empty( \IPSUtf8\Request::i()->controller ) ? 'browser' : \IPSUtf8\Request::i()->controller;
$version = \IPSUtf8\Convert::VERSION_ID;
$copyrightDate = date('Y');
diff --git a/admin/install/css/fontawesome.css b/admin/install/css/fontawesome.css
deleted file mode 100644
index 9660cfe3..00000000
--- a/admin/install/css/fontawesome.css
+++ /dev/null
@@ -1,1338 +0,0 @@
-/*!
- * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome
- * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
- */
-/* FONT PATH
- * -------------------------- */
-@font-face {
- font-family: 'FontAwesome';
- src: url('../font/fontawesome-webfont.eot?v=4.0.3');
- src: url('../font/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'), url('../font/fontawesome-webfont.woff?v=4.0.3') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.0.3') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');
- font-weight: normal;
- font-style: normal;
-}
-.fa {
- display: inline-block;
- font-family: FontAwesome;
- font-style: normal;
- font-weight: normal;
- line-height: 1;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-/* makes the font 33% larger relative to the icon container */
-.fa-lg {
- font-size: 1.3333333333333333em;
- line-height: 0.75em;
- vertical-align: -15%;
-}
-.fa-2x {
- font-size: 2em;
-}
-.fa-3x {
- font-size: 3em;
-}
-.fa-4x {
- font-size: 4em;
-}
-.fa-5x {
- font-size: 5em;
-}
-.fa-fw {
- width: 1.2857142857142858em;
- text-align: center;
-}
-.fa-ul {
- padding-left: 0;
- margin-left: 2.142857142857143em;
- list-style-type: none;
-}
-.fa-ul > li {
- position: relative;
-}
-.fa-li {
- position: absolute;
- left: -2.142857142857143em;
- width: 2.142857142857143em;
- top: 0.14285714285714285em;
- text-align: center;
-}
-.fa-li.fa-lg {
- left: -1.8571428571428572em;
-}
-.fa-border {
- padding: .2em .25em .15em;
- border: solid 0.08em #eeeeee;
- border-radius: .1em;
-}
-.pull-right {
- float: right;
-}
-.pull-left {
- float: left;
-}
-.fa.pull-left {
- margin-right: .3em;
-}
-.fa.pull-right {
- margin-left: .3em;
-}
-.fa-spin {
- -webkit-animation: spin 2s infinite linear;
- -moz-animation: spin 2s infinite linear;
- -o-animation: spin 2s infinite linear;
- animation: spin 2s infinite linear;
-}
-@-moz-keyframes spin {
- 0% {
- -moz-transform: rotate(0deg);
- }
- 100% {
- -moz-transform: rotate(359deg);
- }
-}
-@-webkit-keyframes spin {
- 0% {
- -webkit-transform: rotate(0deg);
- }
- 100% {
- -webkit-transform: rotate(359deg);
- }
-}
-@-o-keyframes spin {
- 0% {
- -o-transform: rotate(0deg);
- }
- 100% {
- -o-transform: rotate(359deg);
- }
-}
-@-ms-keyframes spin {
- 0% {
- -ms-transform: rotate(0deg);
- }
- 100% {
- -ms-transform: rotate(359deg);
- }
-}
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(359deg);
- }
-}
-.fa-rotate-90 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
- -webkit-transform: rotate(90deg);
- -moz-transform: rotate(90deg);
- -ms-transform: rotate(90deg);
- -o-transform: rotate(90deg);
- transform: rotate(90deg);
-}
-.fa-rotate-180 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
- -webkit-transform: rotate(180deg);
- -moz-transform: rotate(180deg);
- -ms-transform: rotate(180deg);
- -o-transform: rotate(180deg);
- transform: rotate(180deg);
-}
-.fa-rotate-270 {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
- -webkit-transform: rotate(270deg);
- -moz-transform: rotate(270deg);
- -ms-transform: rotate(270deg);
- -o-transform: rotate(270deg);
- transform: rotate(270deg);
-}
-.fa-flip-horizontal {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
- -webkit-transform: scale(-1, 1);
- -moz-transform: scale(-1, 1);
- -ms-transform: scale(-1, 1);
- -o-transform: scale(-1, 1);
- transform: scale(-1, 1);
-}
-.fa-flip-vertical {
- filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
- -webkit-transform: scale(1, -1);
- -moz-transform: scale(1, -1);
- -ms-transform: scale(1, -1);
- -o-transform: scale(1, -1);
- transform: scale(1, -1);
-}
-.fa-stack {
- position: relative;
- display: inline-block;
- width: 2em;
- height: 2em;
- line-height: 2em;
- vertical-align: middle;
-}
-.fa-stack-1x,
-.fa-stack-2x {
- position: absolute;
- left: 0;
- width: 100%;
- text-align: center;
-}
-.fa-stack-1x {
- line-height: inherit;
-}
-.fa-stack-2x {
- font-size: 2em;
-}
-.fa-inverse {
- color: #ffffff;
-}
-/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
- readers do not read off random characters that represent icons */
-.fa-glass:before {
- content: "\f000";
-}
-.fa-music:before {
- content: "\f001";
-}
-.fa-search:before {
- content: "\f002";
-}
-.fa-envelope-o:before {
- content: "\f003";
-}
-.fa-heart:before {
- content: "\f004";
-}
-.fa-star:before {
- content: "\f005";
-}
-.fa-star-o:before {
- content: "\f006";
-}
-.fa-user:before {
- content: "\f007";
-}
-.fa-film:before {
- content: "\f008";
-}
-.fa-th-large:before {
- content: "\f009";
-}
-.fa-th:before {
- content: "\f00a";
-}
-.fa-th-list:before {
- content: "\f00b";
-}
-.fa-check:before {
- content: "\f00c";
-}
-.fa-times:before {
- content: "\f00d";
-}
-.fa-search-plus:before {
- content: "\f00e";
-}
-.fa-search-minus:before {
- content: "\f010";
-}
-.fa-power-off:before {
- content: "\f011";
-}
-.fa-signal:before {
- content: "\f012";
-}
-.fa-gear:before,
-.fa-cog:before {
- content: "\f013";
-}
-.fa-trash-o:before {
- content: "\f014";
-}
-.fa-home:before {
- content: "\f015";
-}
-.fa-file-o:before {
- content: "\f016";
-}
-.fa-clock-o:before {
- content: "\f017";
-}
-.fa-road:before {
- content: "\f018";
-}
-.fa-download:before {
- content: "\f019";
-}
-.fa-arrow-circle-o-down:before {
- content: "\f01a";
-}
-.fa-arrow-circle-o-up:before {
- content: "\f01b";
-}
-.fa-inbox:before {
- content: "\f01c";
-}
-.fa-play-circle-o:before {
- content: "\f01d";
-}
-.fa-rotate-right:before,
-.fa-repeat:before {
- content: "\f01e";
-}
-.fa-refresh:before {
- content: "\f021";
-}
-.fa-list-alt:before {
- content: "\f022";
-}
-.fa-lock:before {
- content: "\f023";
-}
-.fa-flag:before {
- content: "\f024";
-}
-.fa-headphones:before {
- content: "\f025";
-}
-.fa-volume-off:before {
- content: "\f026";
-}
-.fa-volume-down:before {
- content: "\f027";
-}
-.fa-volume-up:before {
- content: "\f028";
-}
-.fa-qrcode:before {
- content: "\f029";
-}
-.fa-barcode:before {
- content: "\f02a";
-}
-.fa-tag:before {
- content: "\f02b";
-}
-.fa-tags:before {
- content: "\f02c";
-}
-.fa-book:before {
- content: "\f02d";
-}
-.fa-bookmark:before {
- content: "\f02e";
-}
-.fa-print:before {
- content: "\f02f";
-}
-.fa-camera:before {
- content: "\f030";
-}
-.fa-font:before {
- content: "\f031";
-}
-.fa-bold:before {
- content: "\f032";
-}
-.fa-italic:before {
- content: "\f033";
-}
-.fa-text-height:before {
- content: "\f034";
-}
-.fa-text-width:before {
- content: "\f035";
-}
-.fa-align-left:before {
- content: "\f036";
-}
-.fa-align-center:before {
- content: "\f037";
-}
-.fa-align-right:before {
- content: "\f038";
-}
-.fa-align-justify:before {
- content: "\f039";
-}
-.fa-list:before {
- content: "\f03a";
-}
-.fa-dedent:before,
-.fa-outdent:before {
- content: "\f03b";
-}
-.fa-indent:before {
- content: "\f03c";
-}
-.fa-video-camera:before {
- content: "\f03d";
-}
-.fa-picture-o:before {
- content: "\f03e";
-}
-.fa-pencil:before {
- content: "\f040";
-}
-.fa-map-marker:before {
- content: "\f041";
-}
-.fa-adjust:before {
- content: "\f042";
-}
-.fa-tint:before {
- content: "\f043";
-}
-.fa-edit:before,
-.fa-pencil-square-o:before {
- content: "\f044";
-}
-.fa-share-square-o:before {
- content: "\f045";
-}
-.fa-check-square-o:before {
- content: "\f046";
-}
-.fa-arrows:before {
- content: "\f047";
-}
-.fa-step-backward:before {
- content: "\f048";
-}
-.fa-fast-backward:before {
- content: "\f049";
-}
-.fa-backward:before {
- content: "\f04a";
-}
-.fa-play:before {
- content: "\f04b";
-}
-.fa-pause:before {
- content: "\f04c";
-}
-.fa-stop:before {
- content: "\f04d";
-}
-.fa-forward:before {
- content: "\f04e";
-}
-.fa-fast-forward:before {
- content: "\f050";
-}
-.fa-step-forward:before {
- content: "\f051";
-}
-.fa-eject:before {
- content: "\f052";
-}
-.fa-chevron-left:before {
- content: "\f053";
-}
-.fa-chevron-right:before {
- content: "\f054";
-}
-.fa-plus-circle:before {
- content: "\f055";
-}
-.fa-minus-circle:before {
- content: "\f056";
-}
-.fa-times-circle:before {
- content: "\f057";
-}
-.fa-check-circle:before {
- content: "\f058";
-}
-.fa-question-circle:before {
- content: "\f059";
-}
-.fa-info-circle:before {
- content: "\f05a";
-}
-.fa-crosshairs:before {
- content: "\f05b";
-}
-.fa-times-circle-o:before {
- content: "\f05c";
-}
-.fa-check-circle-o:before {
- content: "\f05d";
-}
-.fa-ban:before {
- content: "\f05e";
-}
-.fa-arrow-left:before {
- content: "\f060";
-}
-.fa-arrow-right:before {
- content: "\f061";
-}
-.fa-arrow-up:before {
- content: "\f062";
-}
-.fa-arrow-down:before {
- content: "\f063";
-}
-.fa-mail-forward:before,
-.fa-share:before {
- content: "\f064";
-}
-.fa-expand:before {
- content: "\f065";
-}
-.fa-compress:before {
- content: "\f066";
-}
-.fa-plus:before {
- content: "\f067";
-}
-.fa-minus:before {
- content: "\f068";
-}
-.fa-asterisk:before {
- content: "\f069";
-}
-.fa-exclamation-circle:before {
- content: "\f06a";
-}
-.fa-gift:before {
- content: "\f06b";
-}
-.fa-leaf:before {
- content: "\f06c";
-}
-.fa-fire:before {
- content: "\f06d";
-}
-.fa-eye:before {
- content: "\f06e";
-}
-.fa-eye-slash:before {
- content: "\f070";
-}
-.fa-warning:before,
-.fa-exclamation-triangle:before {
- content: "\f071";
-}
-.fa-plane:before {
- content: "\f072";
-}
-.fa-calendar:before {
- content: "\f073";
-}
-.fa-random:before {
- content: "\f074";
-}
-.fa-comment:before {
- content: "\f075";
-}
-.fa-magnet:before {
- content: "\f076";
-}
-.fa-chevron-up:before {
- content: "\f077";
-}
-.fa-chevron-down:before {
- content: "\f078";
-}
-.fa-retweet:before {
- content: "\f079";
-}
-.fa-shopping-cart:before {
- content: "\f07a";
-}
-.fa-folder:before {
- content: "\f07b";
-}
-.fa-folder-open:before {
- content: "\f07c";
-}
-.fa-arrows-v:before {
- content: "\f07d";
-}
-.fa-arrows-h:before {
- content: "\f07e";
-}
-.fa-bar-chart-o:before {
- content: "\f080";
-}
-.fa-twitter-square:before {
- content: "\f081";
-}
-.fa-facebook-square:before {
- content: "\f082";
-}
-.fa-camera-retro:before {
- content: "\f083";
-}
-.fa-key:before {
- content: "\f084";
-}
-.fa-gears:before,
-.fa-cogs:before {
- content: "\f085";
-}
-.fa-comments:before {
- content: "\f086";
-}
-.fa-thumbs-o-up:before {
- content: "\f087";
-}
-.fa-thumbs-o-down:before {
- content: "\f088";
-}
-.fa-star-half:before {
- content: "\f089";
-}
-.fa-heart-o:before {
- content: "\f08a";
-}
-.fa-sign-out:before {
- content: "\f08b";
-}
-.fa-linkedin-square:before {
- content: "\f08c";
-}
-.fa-thumb-tack:before {
- content: "\f08d";
-}
-.fa-external-link:before {
- content: "\f08e";
-}
-.fa-sign-in:before {
- content: "\f090";
-}
-.fa-trophy:before {
- content: "\f091";
-}
-.fa-github-square:before {
- content: "\f092";
-}
-.fa-upload:before {
- content: "\f093";
-}
-.fa-lemon-o:before {
- content: "\f094";
-}
-.fa-phone:before {
- content: "\f095";
-}
-.fa-square-o:before {
- content: "\f096";
-}
-.fa-bookmark-o:before {
- content: "\f097";
-}
-.fa-phone-square:before {
- content: "\f098";
-}
-.fa-twitter:before {
- content: "\f099";
-}
-.fa-facebook:before {
- content: "\f09a";
-}
-.fa-github:before {
- content: "\f09b";
-}
-.fa-unlock:before {
- content: "\f09c";
-}
-.fa-credit-card:before {
- content: "\f09d";
-}
-.fa-rss:before {
- content: "\f09e";
-}
-.fa-hdd-o:before {
- content: "\f0a0";
-}
-.fa-bullhorn:before {
- content: "\f0a1";
-}
-.fa-bell:before {
- content: "\f0f3";
-}
-.fa-certificate:before {
- content: "\f0a3";
-}
-.fa-hand-o-right:before {
- content: "\f0a4";
-}
-.fa-hand-o-left:before {
- content: "\f0a5";
-}
-.fa-hand-o-up:before {
- content: "\f0a6";
-}
-.fa-hand-o-down:before {
- content: "\f0a7";
-}
-.fa-arrow-circle-left:before {
- content: "\f0a8";
-}
-.fa-arrow-circle-right:before {
- content: "\f0a9";
-}
-.fa-arrow-circle-up:before {
- content: "\f0aa";
-}
-.fa-arrow-circle-down:before {
- content: "\f0ab";
-}
-.fa-globe:before {
- content: "\f0ac";
-}
-.fa-wrench:before {
- content: "\f0ad";
-}
-.fa-tasks:before {
- content: "\f0ae";
-}
-.fa-filter:before {
- content: "\f0b0";
-}
-.fa-briefcase:before {
- content: "\f0b1";
-}
-.fa-arrows-alt:before {
- content: "\f0b2";
-}
-.fa-group:before,
-.fa-users:before {
- content: "\f0c0";
-}
-.fa-chain:before,
-.fa-link:before {
- content: "\f0c1";
-}
-.fa-cloud:before {
- content: "\f0c2";
-}
-.fa-flask:before {
- content: "\f0c3";
-}
-.fa-cut:before,
-.fa-scissors:before {
- content: "\f0c4";
-}
-.fa-copy:before,
-.fa-files-o:before {
- content: "\f0c5";
-}
-.fa-paperclip:before {
- content: "\f0c6";
-}
-.fa-save:before,
-.fa-floppy-o:before {
- content: "\f0c7";
-}
-.fa-square:before {
- content: "\f0c8";
-}
-.fa-bars:before {
- content: "\f0c9";
-}
-.fa-list-ul:before {
- content: "\f0ca";
-}
-.fa-list-ol:before {
- content: "\f0cb";
-}
-.fa-strikethrough:before {
- content: "\f0cc";
-}
-.fa-underline:before {
- content: "\f0cd";
-}
-.fa-table:before {
- content: "\f0ce";
-}
-.fa-magic:before {
- content: "\f0d0";
-}
-.fa-truck:before {
- content: "\f0d1";
-}
-.fa-pinterest:before {
- content: "\f0d2";
-}
-.fa-pinterest-square:before {
- content: "\f0d3";
-}
-.fa-google-plus-square:before {
- content: "\f0d4";
-}
-.fa-google-plus:before {
- content: "\f0d5";
-}
-.fa-money:before {
- content: "\f0d6";
-}
-.fa-caret-down:before {
- content: "\f0d7";
-}
-.fa-caret-up:before {
- content: "\f0d8";
-}
-.fa-caret-left:before {
- content: "\f0d9";
-}
-.fa-caret-right:before {
- content: "\f0da";
-}
-.fa-columns:before {
- content: "\f0db";
-}
-.fa-unsorted:before,
-.fa-sort:before {
- content: "\f0dc";
-}
-.fa-sort-down:before,
-.fa-sort-asc:before {
- content: "\f0dd";
-}
-.fa-sort-up:before,
-.fa-sort-desc:before {
- content: "\f0de";
-}
-.fa-envelope:before {
- content: "\f0e0";
-}
-.fa-linkedin:before {
- content: "\f0e1";
-}
-.fa-rotate-left:before,
-.fa-undo:before {
- content: "\f0e2";
-}
-.fa-legal:before,
-.fa-gavel:before {
- content: "\f0e3";
-}
-.fa-dashboard:before,
-.fa-tachometer:before {
- content: "\f0e4";
-}
-.fa-comment-o:before {
- content: "\f0e5";
-}
-.fa-comments-o:before {
- content: "\f0e6";
-}
-.fa-flash:before,
-.fa-bolt:before {
- content: "\f0e7";
-}
-.fa-sitemap:before {
- content: "\f0e8";
-}
-.fa-umbrella:before {
- content: "\f0e9";
-}
-.fa-paste:before,
-.fa-clipboard:before {
- content: "\f0ea";
-}
-.fa-lightbulb-o:before {
- content: "\f0eb";
-}
-.fa-exchange:before {
- content: "\f0ec";
-}
-.fa-cloud-download:before {
- content: "\f0ed";
-}
-.fa-cloud-upload:before {
- content: "\f0ee";
-}
-.fa-user-md:before {
- content: "\f0f0";
-}
-.fa-stethoscope:before {
- content: "\f0f1";
-}
-.fa-suitcase:before {
- content: "\f0f2";
-}
-.fa-bell-o:before {
- content: "\f0a2";
-}
-.fa-coffee:before {
- content: "\f0f4";
-}
-.fa-cutlery:before {
- content: "\f0f5";
-}
-.fa-file-text-o:before {
- content: "\f0f6";
-}
-.fa-building-o:before {
- content: "\f0f7";
-}
-.fa-hospital-o:before {
- content: "\f0f8";
-}
-.fa-ambulance:before {
- content: "\f0f9";
-}
-.fa-medkit:before {
- content: "\f0fa";
-}
-.fa-fighter-jet:before {
- content: "\f0fb";
-}
-.fa-beer:before {
- content: "\f0fc";
-}
-.fa-h-square:before {
- content: "\f0fd";
-}
-.fa-plus-square:before {
- content: "\f0fe";
-}
-.fa-angle-double-left:before {
- content: "\f100";
-}
-.fa-angle-double-right:before {
- content: "\f101";
-}
-.fa-angle-double-up:before {
- content: "\f102";
-}
-.fa-angle-double-down:before {
- content: "\f103";
-}
-.fa-angle-left:before {
- content: "\f104";
-}
-.fa-angle-right:before {
- content: "\f105";
-}
-.fa-angle-up:before {
- content: "\f106";
-}
-.fa-angle-down:before {
- content: "\f107";
-}
-.fa-desktop:before {
- content: "\f108";
-}
-.fa-laptop:before {
- content: "\f109";
-}
-.fa-tablet:before {
- content: "\f10a";
-}
-.fa-mobile-phone:before,
-.fa-mobile:before {
- content: "\f10b";
-}
-.fa-circle-o:before {
- content: "\f10c";
-}
-.fa-quote-left:before {
- content: "\f10d";
-}
-.fa-quote-right:before {
- content: "\f10e";
-}
-.fa-spinner:before {
- content: "\f110";
-}
-.fa-circle:before {
- content: "\f111";
-}
-.fa-mail-reply:before,
-.fa-reply:before {
- content: "\f112";
-}
-.fa-github-alt:before {
- content: "\f113";
-}
-.fa-folder-o:before {
- content: "\f114";
-}
-.fa-folder-open-o:before {
- content: "\f115";
-}
-.fa-smile-o:before {
- content: "\f118";
-}
-.fa-frown-o:before {
- content: "\f119";
-}
-.fa-meh-o:before {
- content: "\f11a";
-}
-.fa-gamepad:before {
- content: "\f11b";
-}
-.fa-keyboard-o:before {
- content: "\f11c";
-}
-.fa-flag-o:before {
- content: "\f11d";
-}
-.fa-flag-checkered:before {
- content: "\f11e";
-}
-.fa-terminal:before {
- content: "\f120";
-}
-.fa-code:before {
- content: "\f121";
-}
-.fa-reply-all:before {
- content: "\f122";
-}
-.fa-mail-reply-all:before {
- content: "\f122";
-}
-.fa-star-half-empty:before,
-.fa-star-half-full:before,
-.fa-star-half-o:before {
- content: "\f123";
-}
-.fa-location-arrow:before {
- content: "\f124";
-}
-.fa-crop:before {
- content: "\f125";
-}
-.fa-code-fork:before {
- content: "\f126";
-}
-.fa-unlink:before,
-.fa-chain-broken:before {
- content: "\f127";
-}
-.fa-question:before {
- content: "\f128";
-}
-.fa-info:before {
- content: "\f129";
-}
-.fa-exclamation:before {
- content: "\f12a";
-}
-.fa-superscript:before {
- content: "\f12b";
-}
-.fa-subscript:before {
- content: "\f12c";
-}
-.fa-eraser:before {
- content: "\f12d";
-}
-.fa-puzzle-piece:before {
- content: "\f12e";
-}
-.fa-microphone:before {
- content: "\f130";
-}
-.fa-microphone-slash:before {
- content: "\f131";
-}
-.fa-shield:before {
- content: "\f132";
-}
-.fa-calendar-o:before {
- content: "\f133";
-}
-.fa-fire-extinguisher:before {
- content: "\f134";
-}
-.fa-rocket:before {
- content: "\f135";
-}
-.fa-maxcdn:before {
- content: "\f136";
-}
-.fa-chevron-circle-left:before {
- content: "\f137";
-}
-.fa-chevron-circle-right:before {
- content: "\f138";
-}
-.fa-chevron-circle-up:before {
- content: "\f139";
-}
-.fa-chevron-circle-down:before {
- content: "\f13a";
-}
-.fa-html5:before {
- content: "\f13b";
-}
-.fa-css3:before {
- content: "\f13c";
-}
-.fa-anchor:before {
- content: "\f13d";
-}
-.fa-unlock-alt:before {
- content: "\f13e";
-}
-.fa-bullseye:before {
- content: "\f140";
-}
-.fa-ellipsis-h:before {
- content: "\f141";
-}
-.fa-ellipsis-v:before {
- content: "\f142";
-}
-.fa-rss-square:before {
- content: "\f143";
-}
-.fa-play-circle:before {
- content: "\f144";
-}
-.fa-ticket:before {
- content: "\f145";
-}
-.fa-minus-square:before {
- content: "\f146";
-}
-.fa-minus-square-o:before {
- content: "\f147";
-}
-.fa-level-up:before {
- content: "\f148";
-}
-.fa-level-down:before {
- content: "\f149";
-}
-.fa-check-square:before {
- content: "\f14a";
-}
-.fa-pencil-square:before {
- content: "\f14b";
-}
-.fa-external-link-square:before {
- content: "\f14c";
-}
-.fa-share-square:before {
- content: "\f14d";
-}
-.fa-compass:before {
- content: "\f14e";
-}
-.fa-toggle-down:before,
-.fa-caret-square-o-down:before {
- content: "\f150";
-}
-.fa-toggle-up:before,
-.fa-caret-square-o-up:before {
- content: "\f151";
-}
-.fa-toggle-right:before,
-.fa-caret-square-o-right:before {
- content: "\f152";
-}
-.fa-euro:before,
-.fa-eur:before {
- content: "\f153";
-}
-.fa-gbp:before {
- content: "\f154";
-}
-.fa-dollar:before,
-.fa-usd:before {
- content: "\f155";
-}
-.fa-rupee:before,
-.fa-inr:before {
- content: "\f156";
-}
-.fa-cny:before,
-.fa-rmb:before,
-.fa-yen:before,
-.fa-jpy:before {
- content: "\f157";
-}
-.fa-ruble:before,
-.fa-rouble:before,
-.fa-rub:before {
- content: "\f158";
-}
-.fa-won:before,
-.fa-krw:before {
- content: "\f159";
-}
-.fa-bitcoin:before,
-.fa-btc:before {
- content: "\f15a";
-}
-.fa-file:before {
- content: "\f15b";
-}
-.fa-file-text:before {
- content: "\f15c";
-}
-.fa-sort-alpha-asc:before {
- content: "\f15d";
-}
-.fa-sort-alpha-desc:before {
- content: "\f15e";
-}
-.fa-sort-amount-asc:before {
- content: "\f160";
-}
-.fa-sort-amount-desc:before {
- content: "\f161";
-}
-.fa-sort-numeric-asc:before {
- content: "\f162";
-}
-.fa-sort-numeric-desc:before {
- content: "\f163";
-}
-.fa-thumbs-up:before {
- content: "\f164";
-}
-.fa-thumbs-down:before {
- content: "\f165";
-}
-.fa-youtube-square:before {
- content: "\f166";
-}
-.fa-youtube:before {
- content: "\f167";
-}
-.fa-xing:before {
- content: "\f168";
-}
-.fa-xing-square:before {
- content: "\f169";
-}
-.fa-youtube-play:before {
- content: "\f16a";
-}
-.fa-dropbox:before {
- content: "\f16b";
-}
-.fa-stack-overflow:before {
- content: "\f16c";
-}
-.fa-instagram:before {
- content: "\f16d";
-}
-.fa-flickr:before {
- content: "\f16e";
-}
-.fa-adn:before {
- content: "\f170";
-}
-.fa-bitbucket:before {
- content: "\f171";
-}
-.fa-bitbucket-square:before {
- content: "\f172";
-}
-.fa-tumblr:before {
- content: "\f173";
-}
-.fa-tumblr-square:before {
- content: "\f174";
-}
-.fa-long-arrow-down:before {
- content: "\f175";
-}
-.fa-long-arrow-up:before {
- content: "\f176";
-}
-.fa-long-arrow-left:before {
- content: "\f177";
-}
-.fa-long-arrow-right:before {
- content: "\f178";
-}
-.fa-apple:before {
- content: "\f179";
-}
-.fa-windows:before {
- content: "\f17a";
-}
-.fa-android:before {
- content: "\f17b";
-}
-.fa-linux:before {
- content: "\f17c";
-}
-.fa-dribbble:before {
- content: "\f17d";
-}
-.fa-skype:before {
- content: "\f17e";
-}
-.fa-foursquare:before {
- content: "\f180";
-}
-.fa-trello:before {
- content: "\f181";
-}
-.fa-female:before {
- content: "\f182";
-}
-.fa-male:before {
- content: "\f183";
-}
-.fa-gittip:before {
- content: "\f184";
-}
-.fa-sun-o:before {
- content: "\f185";
-}
-.fa-moon-o:before {
- content: "\f186";
-}
-.fa-archive:before {
- content: "\f187";
-}
-.fa-bug:before {
- content: "\f188";
-}
-.fa-vk:before {
- content: "\f189";
-}
-.fa-weibo:before {
- content: "\f18a";
-}
-.fa-renren:before {
- content: "\f18b";
-}
-.fa-pagelines:before {
- content: "\f18c";
-}
-.fa-stack-exchange:before {
- content: "\f18d";
-}
-.fa-arrow-circle-o-right:before {
- content: "\f18e";
-}
-.fa-arrow-circle-o-left:before {
- content: "\f190";
-}
-.fa-toggle-left:before,
-.fa-caret-square-o-left:before {
- content: "\f191";
-}
-.fa-dot-circle-o:before {
- content: "\f192";
-}
-.fa-wheelchair:before {
- content: "\f193";
-}
-.fa-vimeo-square:before {
- content: "\f194";
-}
-.fa-turkish-lira:before,
-.fa-try:before {
- content: "\f195";
-}
-.fa-plus-square-o:before {
- content: "\f196";
-}
diff --git a/admin/install/css/global.css b/admin/install/css/global.css
index 2399e269..b2d7b063 100644
--- a/admin/install/css/global.css
+++ b/admin/install/css/global.css
@@ -178,7 +178,7 @@ a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
background: #5490c0;
position: relative;
- padding-left: 6px;
+ padding-left: 6px;
}
.ipsProgressBar_progress[data-progress]:after {
diff --git a/admin/install/css/messages.css b/admin/install/css/messages.css
index 4184be36..3727d543 100644
--- a/admin/install/css/messages.css
+++ b/admin/install/css/messages.css
@@ -13,7 +13,7 @@
left: 15px;
font-size: 20px;
}
-
+
.ipsMessage_error {
background: #b52b38;
}
diff --git a/admin/install/emoticons/excl.png b/admin/install/emoticons/excl.png
deleted file mode 100644
index f488bb53..00000000
Binary files a/admin/install/emoticons/excl.png and /dev/null differ
diff --git a/admin/install/eula.txt b/admin/install/eula.txt
index 9721e9f4..bcf952d7 100644
--- a/admin/install/eula.txt
+++ b/admin/install/eula.txt
@@ -1,89 +1 @@
-=============================[NOTE]=============================
- This software is Nulled!
- Nulled by IPBMafia.ru
- Buy license at https://www.invisionpower.com/buy/self-hosted
-================================================================
- IPS 4.1.13.1 Release (101038)
-=============================[NOTE]=============================
-
-Invision Power Services End User License Agreement
-
-
-1. Acceptance
-
-This software license agreement (the “Agreement”) is between you (either an individual or entity, hereinafter referred to as “You” or “Your”) and Invision Power Services, Inc. (“IPS”, “We”, “Our” or “Us”) and pertains to the IPS Community Suite software, its components, features, enhancements, updates and modifications (collectively the “Software” or “Suite”) and any related services (“Services”) provided by IPS under this Agreement.
-
-By purchasing, downloading, accessing, installing, facilitating the installation or using the Software or Services (or allowing or authorizing any other person to do so) you are warranting that you have the authority provided by your state, country or jurisdiction to enter into a legally binding contract and that you have read and agree to be bound by the terms of this Agreement. You understand and acknowledge that this Agreement comprises the entire agreement between You and IPS. This Agreement supersedes any prior agreements, promises, representations or descriptions. If you do not agree with the terms and conditions set forth hereinafter, you may not purchase, download, access, install or use the Software or Services. This Agreement may only be modified in writing, signed by an officer of IPS and You.
-
-
-2. License Scope and Grant
-
-In consideration for payment of all applicable license fees and subject to the terms and conditions of this Agreement, IPS grants you a revocable, limited and non-exclusive license to install and use the Software and Services on a single installation, accessible via one URL. Access to services, components, software updates and technical support require an active license. All rights in and to the Software and Services are reserved to the use and benefit of IPS and/or its licensors, successors and assigners.
-
-(a) Test Installation. You may install and test one additional instance of the Software for the purpose of development and testing. This installation must be and remain isolated and inaccessible to the general public at all times.
-
-
-3. Restrictions
-
-Your license to use the Software and Services is subject to the following terms, conditions and restrictions on use:
-
-(a) You may alter and modify the Software’s source code for Your use, subject to this Agreement, however, you may not distribute, publish, resell, or otherwise share the Software or derivative works based on the Software without prior express written consent from IPS.
-
-(b) You may not utilize the Software or Services to engage in, facilitate, or otherwise allow others to engage in any activity that violates any law or regulation or the terms and conditions within this Agreement.
-
-(c) You are solely responsible for complying with all governmental regulations and policies. You agree to indemnify Us from any loss, action or damage arising from Your failure to use the Software in a manner inconsistent with applicable legislation.
-
-(d) You may not remove any copyright or proprietary notices on or within the software’s source code at any time. A copyright removal license may be purchased to remove the public display of the Software’s publicly displayed copyright notice.
-
-
-4. Intellectual Property
-
-We have the sole and exclusive right to the Software which is being licensed, not sold, to you by IPS under the terms and conditions of this agreement. The Software is protected by copyright, trademark and other intellectual property laws. IPS reserves any rights not expressly granted herein.
-
-
-5. Assignment
-
-You may not rent, lease, sell, share or authorize or temporarily assign your rights to the license or this Agreement to another individual or entity except as expressly permitted herein.
-
-(a) You may, after six months from the original purchase date, permanently transfer your licensing rights and interest in this Agreement to a third party, provided the license is active and in good standing. Any transfer and reassignment must occur via the mechanisms provided within the invisionpower.com client area and all applicable fees must be paid for the reassignment to be considered valid.
-
-(b) You agree to provide this Agreement in its entirety to the assignee prior to reassignment and the assignee must agree to be bound to the terms of this Agreement.
-
-(c) You acknowledge that account and non license specific materials and purchases, including, but not limited to: account credits and third party (“marketplace”) add-ons and purchases are non-transferrable under any circumstances.
-
-(d) We reserve the right to refuse or reject reassignment at our sole discretion.
-
-
-6. Termination
-
-This Agreement shall remain in effect until terminated. Your rights under this Agreement will automatically terminate without notice from Us in the event you fail to comply with any term or condition within this Agreement. Upon termination of this Agreement, you shall immediately cease all use of the Software and destroy all copies, full or partial, of the Software that may be in your possession or otherwise under your control.
-
-
-7. Modification
-
-We reserve the right to amend or modify the terms of this Agreement at any time, and to change, discontinue or impose conditions on any aspect of the Software or related Service and to provide notification solely by posting an updated version of the Agreement on the invisionpower.com website. You acknowledge that you are solely responsible for regularly reviewing this Agreement and our policies. Continued use of the Software after modification to the Agreement constitutes your binding consent to such changes.
-
-
-8. Disclaimer of Warranty
-
-THE SOFTWARE IS PROVIDED “AS-IS” AND TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, WE DISCLAIM ALL OTHER WARRANTIES, CONDITIONS, REPRESENTATIONS OR TERMS, EXPRESS OR IMPLIED, BY STATUTE, COMMON LAW, USAGE OR OTHERWISE, REGARDING THE SOFTWARE AND ANY RELATED SERVICES, INCLUDING THEIR FITNESS FOR A PARTICULAR PURPOSE, THEIR QUALITY, THEIR MERCHANTABILITY, OR THEIR NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SOFTWARE OR ANY RELATED SERVICES IS SECURE, OR IS FREE FROM BUGS, VIRUSES, ERRORS, OR OTHER PROGRAM LIMITATIONS, OR THAT DEFECTS WILL BE CORRECTED. SOME JURISDICTIONS DO NOT ALLOW EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSIONS MAY NOT APPLY TO YOU. THE ENTIRE RISK AS TO THE RESULTS, QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR OUR EMPLOYEES, REPRESENTATIVE OR AGENTS OR THROUGH OR FROM THE SOFTWARE OR OUR WEBSITE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED WITHIN. THIS SECTION WILL SURVIVE THE TERMINATION OF THIS AGREEMENT.
-
-
-9. Limitation of Liability
-
-TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL WE, OUR SUBSIDIARIES, OFFICERS, DIRECTORS, SHAREHOLDERS, PARTNERS, EMPLOYEES, AGENTS, CONTRACTORS OR OUR SUCCESSORS BE LIABLE FOR ANY DAMAGES WHATSOEVER, INCLUDING, BUT NOT LIMITED TO PERSONAL INJURY OR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY, PUNITIVE, COVER, CONSEQUENTIAL OR OTHER DAMAGES, INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF PROFITS, LOSS OF DATA, LOSS OF USE, COSTS OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, LOSS OF GOODWILL OR ANY OTHER COMMERCIAL DAMAGES OR LOSSES, ARISING OUT OF OR RELATED TO THE USE OF, OR INABILITY TO USE, THE SOFTWARE OR ANY RELATED SERVICES OR CONTENT, HOWEVER CAUSED, REGARDLESS OF THE THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND EVEN IF A REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE. WITHOUT LIMITING THE FOREGOING, IN NO EVENT SHALL OUR TOTAL LIABILITY TO YOU FOR ACTUAL DAMAGES FOR ANY CAUSE WHATSOEVER EXCEED THE AMOUNT PAID BY YOU TO LICENSE THE SOFTWARE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE FOREGOING LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU. THESE LIMITATIONS WILL SURVIVE THE TERMINATION OF THIS AGREEMENT.
-
-
-10. Indemnification
-
-You agree to fully indemnify and hold us, our successors, officers, directors, shareholders, partners, employees, agents, contracts and our successors harmless from and against any claim, suit, hearing, action, expense or demand, including without limitation to: all claims for damages, fees or costs (including attorneys’ fees), arising out of or related to the use of the Software and/or any related Services by you, your agents or representatives, anyone under your control, or by any third party using your equipment or accounts to use the Software or Services (for purposes of this section, collectively, “you”); the violation by you of any provision of this Agreement; the violation by you and any laws or regulations; or the infringement or misappropriation by you any copyright, trademark or any other intellectual property right, proprietary right, property right or any other right of ours or any third party. We reserve the right to assume the exclusive defense and control of any matter otherwise subject to indemnification by you, in which event you will cooperate with us in asserting any available defenses and, as set forth above, be fully responsible for our attorneys’ fees.
-
-
-11. Governing Law
-
-The license and this Agreement are governed by and construed in accordance with the laws of the State of Virginia, United States of America. You hereby consent to exclusive jurisdiction and venue in the County of Bedford and State of Virginia. By agreeing to the terms of this Agreement, you are waiving any claims that you might otherwise have against IPS based on the laws of other jurisdictions.
-
-
-12. Severability
-
-If, for any reason, a court of competent jurisdiction deems any provision or part of this Agreement to be unlawful or unenforceable, the remainder of the Agreement shall remain in full force and effect.
+=============================[NOTE]=============================
This software is Nulled!
Nulled by IPBMafia.ru
Buy license at https://www.invisionpower.com/buy/self-hosted
================================================================
IPS 4.1.19.3 Release (101098)
=============================[NOTE]=============================
Invision Power Services End User License Agreement
1. Acceptance
This software license agreement (the “Agreement”) is between you (either an individual or entity, hereinafter referred to as “You” or “Your”) and Invision Power Services, Inc. (“IPS”, “We”, “Our” or “Us”) and pertains to the IPS Community Suite software, its components, features, enhancements, updates and modifications (collectively the “Software” or “Suite”) and any related services (“Services”) provided by IPS under this Agreement.
By purchasing, downloading, accessing, installing, facilitating the installation or using the Software or Services (or allowing or authorizing any other person to do so) you are warranting that you have the authority provided by your state, country or jurisdiction to enter into a legally binding contract and that you have read and agree to be bound by the terms of this Agreement. You understand and acknowledge that this Agreement comprises the entire agreement between You and IPS. This Agreement supersedes any prior agreements, promises, representations or descriptions. If you do not agree with the terms and conditions set forth hereinafter, you may not purchase, download, access, install or use the Software or Services. This Agreement may only be modified in writing, signed by an officer of IPS and You.
2. License Scope and Grant
In consideration for payment of all applicable license fees and subject to the terms and conditions of this Agreement, IPS grants you a revocable, limited and non-exclusive license to install and use the Software and Services on a single installation, accessible via one URL. Access to services, components, software updates and technical support require an active license. All rights in and to the Software and Services are reserved to the use and benefit of IPS and/or its licensors, successors and assigners.
(a) Test Installation. You may install and test one additional instance of the Software for the purpose of development and testing. This installation must be and remain isolated and inaccessible to the general public at all times.
3. Restrictions
Your license to use the Software and Services is subject to the following terms, conditions and restrictions on use:
(a) You may alter and modify the Software’s source code for Your use, subject to this Agreement, however, you may not distribute, publish, resell, or otherwise share the Software or derivative works based on the Software without prior express written consent from IPS.
(b) You may not utilize the Software or Services to engage in, facilitate, or otherwise allow others to engage in any activity that violates any law or regulation or the terms and conditions within this Agreement.
(c) You are solely responsible for complying with all governmental regulations and policies. You agree to indemnify Us from any loss, action or damage arising from Your failure to use the Software in a manner inconsistent with applicable legislation.
(d) You may not remove any copyright or proprietary notices on or within the software’s source code at any time. A copyright removal license may be purchased to remove the public display of the Software’s publicly displayed copyright notice.
4. Intellectual Property
We have the sole and exclusive right to the Software which is being licensed, not sold, to you by IPS under the terms and conditions of this agreement. The Software is protected by copyright, trademark and other intellectual property laws. IPS reserves any rights not expressly granted herein.
5. Assignment
You may not rent, lease, sell, share or authorize or temporarily assign your rights to the license or this Agreement to another individual or entity except as expressly permitted herein.
(a) You may, after six months from the original purchase date, permanently transfer your licensing rights and interest in this Agreement to a third party, provided the license is active and in good standing. Any transfer and reassignment must occur via the mechanisms provided within the invisionpower.com client area and all applicable fees must be paid for the reassignment to be considered valid.
(b) You agree to provide this Agreement in its entirety to the assignee prior to reassignment and the assignee must agree to be bound to the terms of this Agreement.
(c) You acknowledge that account and non license specific materials and purchases, including, but not limited to: account credits and third party (“marketplace”) add-ons and purchases are non-transferrable under any circumstances.
(d) We reserve the right to refuse or reject reassignment at our sole discretion.
6. Termination
This Agreement shall remain in effect until terminated. Your rights under this Agreement will automatically terminate without notice from Us in the event you fail to comply with any term or condition within this Agreement. Upon termination of this Agreement, you shall immediately cease all use of the Software and destroy all copies, full or partial, of the Software that may be in your possession or otherwise under your control.
7. Modification
We reserve the right to amend or modify the terms of this Agreement at any time, and to change, discontinue or impose conditions on any aspect of the Software or related Service and to provide notification solely by posting an updated version of the Agreement on the invisionpower.com website. You acknowledge that you are solely responsible for regularly reviewing this Agreement and our policies. Continued use of the Software after modification to the Agreement constitutes your binding consent to such changes.
8. Disclaimer of Warranty
THE SOFTWARE IS PROVIDED “AS-IS” AND TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, WE DISCLAIM ALL OTHER WARRANTIES, CONDITIONS, REPRESENTATIONS OR TERMS, EXPRESS OR IMPLIED, BY STATUTE, COMMON LAW, USAGE OR OTHERWISE, REGARDING THE SOFTWARE AND ANY RELATED SERVICES, INCLUDING THEIR FITNESS FOR A PARTICULAR PURPOSE, THEIR QUALITY, THEIR MERCHANTABILITY, OR THEIR NON-INFRINGEMENT. WE DO NOT WARRANT THAT THE SOFTWARE OR ANY RELATED SERVICES IS SECURE, OR IS FREE FROM BUGS, VIRUSES, ERRORS, OR OTHER PROGRAM LIMITATIONS, OR THAT DEFECTS WILL BE CORRECTED. SOME JURISDICTIONS DO NOT ALLOW EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSIONS MAY NOT APPLY TO YOU. THE ENTIRE RISK AS TO THE RESULTS, QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. NO ADVICE OR INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR OUR EMPLOYEES, REPRESENTATIVE OR AGENTS OR THROUGH OR FROM THE SOFTWARE OR OUR WEBSITE SHALL CREATE ANY WARRANTY NOT EXPRESSLY STATED WITHIN. THIS SECTION WILL SURVIVE THE TERMINATION OF THIS AGREEMENT.
9. Limitation of Liability
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL WE, OUR SUBSIDIARIES, OFFICERS, DIRECTORS, SHAREHOLDERS, PARTNERS, EMPLOYEES, AGENTS, CONTRACTORS OR OUR SUCCESSORS BE LIABLE FOR ANY DAMAGES WHATSOEVER, INCLUDING, BUT NOT LIMITED TO PERSONAL INJURY OR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY, PUNITIVE, COVER, CONSEQUENTIAL OR OTHER DAMAGES, INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF PROFITS, LOSS OF DATA, LOSS OF USE, COSTS OF PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, BUSINESS INTERRUPTION, LOSS OF GOODWILL OR ANY OTHER COMMERCIAL DAMAGES OR LOSSES, ARISING OUT OF OR RELATED TO THE USE OF, OR INABILITY TO USE, THE SOFTWARE OR ANY RELATED SERVICES OR CONTENT, HOWEVER CAUSED, REGARDLESS OF THE THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR OTHERWISE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND EVEN IF A REMEDY SET FORTH HEREIN IS FOUND TO HAVE FAILED OF ITS ESSENTIAL PURPOSE. WITHOUT LIMITING THE FOREGOING, IN NO EVENT SHALL OUR TOTAL LIABILITY TO YOU FOR ACTUAL DAMAGES FOR ANY CAUSE WHATSOEVER EXCEED THE AMOUNT PAID BY YOU TO LICENSE THE SOFTWARE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE FOREGOING LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU. THESE LIMITATIONS WILL SURVIVE THE TERMINATION OF THIS AGREEMENT.
10. Indemnification
You agree to fully indemnify and hold us, our successors, officers, directors, shareholders, partners, employees, agents, contracts and our successors harmless from and against any claim, suit, hearing, action, expense or demand, including without limitation to: all claims for damages, fees or costs (including attorneys’ fees), arising out of or related to the use of the Software and/or any related Services by you, your agents or representatives, anyone under your control, or by any third party using your equipment or accounts to use the Software or Services (for purposes of this section, collectively, “you”); the violation by you of any provision of this Agreement; the violation by you and any laws or regulations; or the infringement or misappropriation by you any copyright, trademark or any other intellectual property right, proprietary right, property right or any other right of ours or any third party. We reserve the right to assume the exclusive defense and control of any matter otherwise subject to indemnification by you, in which event you will cooperate with us in asserting any available defenses and, as set forth above, be fully responsible for our attorneys’ fees.
11. Governing Law
The license and this Agreement are governed by and construed in accordance with the laws of the State of Virginia, United States of America. You hereby consent to exclusive jurisdiction and venue in the County of Bedford and State of Virginia. By agreeing to the terms of this Agreement, you are waiving any claims that you might otherwise have against IPS based on the laws of other jurisdictions.
12. Severability
If, for any reason, a court of competent jurisdiction deems any provision or part of this Agreement to be unlawful or unenforceable, the remainder of the Agreement shall remain in full force and effect.
\ No newline at end of file
diff --git a/admin/install/html/global/globalTemplate.phtml b/admin/install/html/global/globalTemplate.phtml
index bd928290..1454b16f 100644
--- a/admin/install/html/global/globalTemplate.phtml
+++ b/admin/install/html/global/globalTemplate.phtml
@@ -21,7 +21,6 @@
-
-
-{{endif}}]]>
-
- $('.elNavigation_app_chat_Chat').click( function( e ){
- e.preventDefault();
-
- var joinWith = '?';
-
- if ( $( e.currentTarget ).attr('href').indexOf('?') != -1 )
- {
- joinWith = '&';
- }
-
- window.open( $( e.currentTarget ).attr('href') + joinWith + '_popup=1', '_blank', "height=480,width=1200,location=0,menubar=0,toolbar=0" );
- return false;
- });
-]]>
-
-
- {lang="to"}
-
-]]>
-
- {lang="chatroom_rules"}
-
- {lang="ipschat_rules"}
-
- {$form|raw}
-]]>
- ipchat_new_window}}
-
-{{endif}}
-
-
-
-
-
-
-
-
-
- {{$leave = \IPS\Request::i()->_popup ? '&_popup=' . \IPS\Request::i()->_popup : '';}}
- {lang="chat_leave_room"}
-
-
-
-
-
-
-
-
-
- {{foreach $chatters as $chatter}}
- {template="userRow" group="view" app="chat" params="$chatter"}
- {{endforeach}}
-
-
-
-
-
-
-
- {{$leave = \IPS\Request::i()->_popup ? '&_popup=' . \IPS\Request::i()->_popup : '';}}
- {lang="chat_leave_room"}
-
-
-
-
-
-{{if member.isAdmin() or !empty( member.modPermissions() )}}
-
- {lang="ipschat_closure_warning" sprintf="\IPS\DateTime::ts( 1493596800 )->format( 'j M Y' )"}
- {lang="ipschat_closure_warning_2"}
-
-{{endif}}]]>
-
- // System vars
- ips.setSetting( 'roomID', {$options['roomId']} );
- ips.setSetting( 'userID', {$options['userId']} );
- ips.setSetting( 'forumUserID', {expression="intval( \IPS\Member::loggedIn()->member_id )"} );
- ips.setSetting( 'username', '{{if \IPS\Member::loggedIn()->member_id}}{expression="\IPS\Member::loggedIn()->name"}{{else}}{expression="\IPS\Member::loggedIn()->language()->get('guest')"}_{$options['userId']}{{endif}}' );
- ips.setSetting( 'accessKey', '{$options['accessKey']}' );
- ips.setSetting( 'serverHost', '{$options['serverHost']}' );
- ips.setSetting( 'serverPath', '{$options['serverPath']}' );
- ips.setSetting( 'ourURL', "{expression="urlencode(\IPS\Settings::i()->base_url)"}" );
-
- ips.setSetting( 'chatEmoticons',
- {expression="json_encode( $emoticons )" raw="true"}
- );
-
- ips.setSetting( 'ignoredChats',
- {expression="json_encode( $ignored )" raw="true"}
- );
-
- ips.setSetting( 'forumIDMap',
- {expression="json_encode( $forumIdMap )" raw="true"}
- );
-
- ips.setSetting( 'groups',
- {expression="json_encode( $groups )" raw="true"}
- );
-
- ips.setSetting( 'badwords',
- {expression="json_encode( $badwords )" raw="true"}
- );
-
- // Sounds in MP3 format
- ips.setSetting( 'sounds', {
- 'enter': 'enter',
- 'leave': 'leave',
- 'message': 'message'
- });
-
- // Settings
- ips.setSetting( 'condenseTime', true );
- ips.setSetting( 'maxMessages', 100 );
- ips.setSetting( 'moderator', {{if \IPS\Member::loggedIn()->group['chat_moderate']}}true{{else}}false{{endif}} );
- ips.setSetting( 'private', {{if \IPS\Member::loggedIn()->group['chat_private']}}true{{else}}false{{endif}} );
- ips.setSetting( 'hideEnterExit', '{expression="(bool) \IPS\Settings::i()->ipchat_hide_usermessage"}' );
- ips.setSetting( 'noBuffer', {{if \IPS\Settings::i()->ipchat_no_buffer}}false{{else}}true{{endif}} );
- ips.setSetting( 'inactiveKick', parseInt( {expression="\IPS\Settings::i()->ipchat_inactive_minutes"} * 60 ) );
- ips.setSetting( 'user24hour', {{if \IPS\Settings::i()->ipchat_24hour == '24'}}true{{else}}false{{endif}} );
- ips.setSetting( 'soundEnabled', ips.utils.db.get( 'chat', 'sounds' ) );
-]]>
- member_group_id );}}
-
-
-
- {$group->formatName( $member->name )|raw}
-
-
-
- {template="userPhoto" group="global" app="core" params="$member, 'medium'"}
-]]>
-
- {{if $data['forumUserID'] or \IPS\Member::loggedIn()->member_id !== $data['forumUserID']}}
-
-
-
- {$data['formattedUsername']|raw}
-
-
-
- {{else}}
-
-
-
- {$data['formattedUsername']|raw}
-
-
- {{endif}}
-]]>
-
- {lang="block_chatwhoschatting"}
-
-]]>
- .ipsTabs_panel {
- height: auto;
- }
-
- #elChatTabBar .ipsTabs_item [data-action="closeTab"] {
- margin-left: 10px;
- }
-
- .cEmoticonsButton, .cSendButton {
- margin-bottom: 0 !important;
- }
-
- .cEmoticonsButton {
- width: 25% !important;
- float: left;
- }
-
- .cSendButton {
- width: 71% !important;
- float: left;
- margin-left: 4% !important;
- }
-
- /* Row stuff */
- .cChatRow {
- display: block;
- }
-
- .cChatRow_date {
- display: none !important;
- }
-
- .cChatRow_responsiveDate {
- float: right;
- font-size: 11px;
- }
-
- .cChatRow_notice {
- text-align: center;
- }
-
- .cChatRow .cChatRow_user {
- text-align: left;
- background: #f5f5f5;
- width: 100%;
- border-bottom: 0;
- min-width: 0;
- max-width: 100%;
- font-size: 13px;
- padding: 7px 10px;
- }
-
- .cChatRow_own .cChatRow_user {
- background: #d0dbe7;
- }
-
- .cChatRow .cChatRow_user:empty {
- display: none;
- }
-
- .cChatRow .cChatRow_user, .cChatRow .cChatRow_content, .cChatRow .cChatRow_date {
- display: block;
- }
-}]]>
- .ipsTabs_panel {
- height: 430px;
-}
-
-#elChatTabBar {
- margin-right: 190px;
-}
-
- #elChatTabBar > li > a:not( .ipsTabs_activeItem ) {
- background: rgba(225,225,225,0.3);
- }
-
-@media screen and (min-width: 767px) {
- #elChatTabBar > li > a:not( .ipsTabs_activeItem ) {
- color: rgba(255,255,255,0.5);
- }
-}
-
-#elUserList {
- max-height: 395px;
- overflow: auto;
-}
-
-.cChatCompose {
-
-}
-
-.ipsApp .cMessageTextarea {
- width: 100%;
- height: 44px;
- resize: none;
-}
-
-.cChatTab {
- position: relative;
- height: 430px;
- border-right: 1px solid rgba(0,0,0,0.1);
-}
-
-.cChatCompose {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
-}
-
- .cChatCompose .ipsColumn_veryNarrow {
- width: 58px;
- }
-
- .cChatCompose .ipsColumn_veryNarrow .ipsButton {
- font-size: 18px;
- }
-
-.cChatUserRow {
- display: block;
-}
-
- .cChatUserRow:hover, .cChatUserRow_hover {
- background: #fafafa;
- }
-
-.cChatUsername {
- font-size: 14px;
- line-height: 28px;
-}
-
- #elUserList .ipsUserPhoto_tiny img,
- #elUserList img.ipsUserPhoto_tiny,
- #elUserList .ipsUserPhoto_tiny:after {
- width: 28px;
- height: 28px;
- }
-
-.cChatUserInfo {
- min-height: 150px;
-}
-
-#elChatTabs [data-action='toggleSound'] {
- font-size: 15px;
-}
-
-/* Message styles */
-.cChatContainer {
- width: 100%;
- height: 355px;
- overflow: auto;
-}
-
-.cChatRow {
- font-size: 14px;
- overflow: auto;
- display: table-row;
- width: 100%;
-}
-
-.cChatRow .cChatRow_user,
-.cChatRow .cChatRow_content,
-.cChatRow .cChatRow_date {
- display: table-cell;
- padding: 10px;
- border-bottom: 1px solid #efefef;
- vertical-align: top;
-}
-
-.cChatRow .cChatRow_user {
- text-align: right;
- color: #898989;
- min-width: 130px;
- max-width: 150px;
- text-overflow: ellipsis;
- overflow: hidden;
-}
-
-.cChatRow_date {
- padding: 0 10px;
- font-size: 12px;
- color: #939393;
- min-width: 75px; /* Wide enough for AM/PM */
- width: 75px;
-}
-/*.cChatRow.cChatRow_date {
- font-size: 12px;
- font-weight: bold;
- color: #222;
-}
-
- .cChatRow.cChatRow_date .cChatRow_user,
- .cChatRow.cChatRow_date .cChatRow_content {
- padding: 5px 10px 0;
- border-bottom: 0px;
- }*/
-
-.cChatRow.cChatRow_notice {
- color: #939393;
-}
-
-.cChatRow.cChatRow_me {
- color: #898989;
-}
-
-.cChatRow .cChatRow_content {
- width: 100%;
-}
-
-.cChatRow_system {
- background: #f9e4e1;
-}
-
-.cChatRow_own {
- background: #e1eaf5;
-}
-
- .cChatRow_own .cChatRow_user,
- .cChatRow_own .cChatRow_content,
- .cChatRow_own .cChatRow_date {
- border-color: #cdd8e6;
- }
-
-
-.cChatParsedImage img {
- max-width: 100px;
- max-height: 100px;
-}
-
-.cChatParsedImage a {
- display: inline-block;
-}
-
- .cChatParsedImage a:last-child {
- vertical-align: top;
- }
-
-.cChatParsedImage .ipsButton {
- line-height: 14px;
- padding: 0 5px;
-}]]>
-
diff --git a/applications/chat/data/versions.json b/applications/chat/data/versions.json
deleted file mode 100644
index b15bd816..00000000
--- a/applications/chat/data/versions.json
+++ /dev/null
@@ -1,176 +0,0 @@
-{
- "11004": "1.1.0 Alpha 5",
- "11005": "1.1.0 Beta 1",
- "11008": "1.1.0",
- "13000": "1.3.0 Beta 1",
- "40000": "4.0.0",
- "100000": "4.0.0 Beta 0",
- "100001": "4.0.0 Beta 1",
- "100002": "4.0.0 Beta 2",
- "100003": "4.0.0 Beta 3",
- "100004": "4.0.0 Beta 3a",
- "100005": "4.0.0 Beta 4",
- "100006": "4.0.0 Beta 4a",
- "100007": "4.0.0 Beta 4b",
- "100008": "4.0.0 Beta 5",
- "100009": "4.0.0 Beta 5a",
- "100010": "4.0.0 Beta 6",
- "100011": "4.0.0 Beta 7",
- "100012": "4.0.0 Beta 7a",
- "100013": "4.0.0 Beta 8",
- "100014": "4.0.0 RC1",
- "100015": "4.0.0 RC2",
- "100016": "4.0.0 RC3",
- "100017": "4.0.0 RC4",
- "100018": "4.0.0 RC5",
- "100019": "4.0.0 RC5a",
- "100020": "4.0.0 RC6",
- "10000": "1.0.0 Alpha 1",
- "10001": "1.0.0 Alpha 2",
- "10002": "1.0.0 Alpha 3",
- "10003": "1.0.0 Alpha 4",
- "10004": "1.0.0 Alpha 5",
- "10005": "1.0.0 Alpha 6",
- "10006": "1.0.0 Alpha 7",
- "10007": "1.0.0 Alpha 8",
- "10008": "1.0.0 Beta 1",
- "10009": "1.0.0 Beta 2",
- "10010": "1.0.0 Beta 3",
- "10011": "1.0.0 Beta 4",
- "10012": "1.0.0 RC 1",
- "10013": "1.0.0 RC 2",
- "10014": "1.0.0",
- "11000": "1.1.0 Alpha 1",
- "11001": "1.1.0 Alpha 2",
- "11002": "1.1.0 Alpha 3",
- "11003": "1.1.0 Alpha 4",
- "11006": "1.1.0 Beta 2",
- "11007": "1.1.0 Beta 3",
- "11009": "1.1.1",
- "11010": "1.1.2",
- "12000": "1.2.0 Beta 1",
- "12001": "1.2.0 RC 1",
- "12002": "1.2.0",
- "13001": "1.3.0 Beta 2",
- "13002": "1.3.0",
- "13003": "1.3.1",
- "14000": "1.4.0 Beta 1",
- "14001": "1.4.0 Beta 2",
- "14002": "1.4.0",
- "14003": "1.4.1",
- "14004": "1.4.2",
- "14005": "1.4.3",
- "14006": "1.4.4",
- "100021": "4.0.0 RC7",
- "100022": "4.0.0 RC7a",
- "100023": "4.0.0",
- "100024": "4.0.1",
- "100025": "4.0.2",
- "100026": "4.0.3",
- "100027": "4.0.4",
- "100028": "4.0.5",
- "100029": "4.0.5.1",
- "100030": "4.0.6",
- "100031": "4.0.6.1",
- "100032": "4.0.7",
- "100033": "4.0.8",
- "100034": "4.0.8.1",
- "100035": "4.0.9",
- "100036": "4.0.9.1",
- "100037": "4.0.9.2",
- "100038": "4.0.10",
- "100039": "4.0.10.1",
- "100040": "4.0.10.2",
- "100041": "4.0.11",
- "100042": "4.0.12",
- "100043": "4.0.12.1",
- "100044": "4.0.13",
- "100045": "4.0.13.1",
- "101000": "4.1.0 Beta 1",
- "101001": "4.1.0 Beta 2",
- "101002": "4.1.0 Beta 3",
- "101003": "4.1.0 Beta 4",
- "101004": "4.1.0 Beta 5",
- "101005": "4.1.0 Beta 6",
- "101006": "4.1.0 Beta 7",
- "101007": "4.1.0 Beta 8",
- "101008": "4.1.0 Beta 9",
- "101009": "4.1.0 Beta 10",
- "101010": "4.1.0",
- "101011": "4.1.1",
- "101012": "4.1.2",
- "101013": "4.1.3",
- "101014": "4.1.3.1",
- "101015": "4.1.3.2",
- "101016": "4.1.4",
- "101017": "4.1.4.1",
- "101018": "4.1.5",
- "101019": "4.1.5.1",
- "101020": "4.1.5.2",
- "101021": "4.1.6",
- "101022": "4.1.6.1",
- "101023": "4.1.7",
- "101024": "4.1.8",
- "101025": "4.1.8.1",
- "101026": "4.1.9",
- "101027": "4.1.10",
- "101028": "4.1.11",
- "101029": "4.1.11.1",
- "101030": "4.1.12",
- "101031": "4.1.12.1",
- "101032": "4.1.12.2",
- "101033": "4.1.12.3",
- "101034": "4.1.13 Beta 1",
- "101035": "4.1.13 Beta 2",
- "101036": "4.1.13",
- "101037": "4.1.13",
- "101038": "4.1.13.1",
- "101040": "4.1.13.2",
- "101041": "4.1.13.2",
- "101042": "4.1.14 Beta 1",
- "101043": "4.1.14 Beta 2",
- "101044": "4.1.14 Beta 3",
- "101045": "4.1.14 Beta 4",
- "101046": "4.1.14 Beta 5",
- "101049": "4.1.14 Beta 6",
- "101050": "4.1.14 Beta 7",
- "101051": "4.1.14 RC",
- "101052": "4.1.14",
- "101053": "4.1.14.1",
- "101054": "4.1.14.2",
- "101055": "4.1.14.3",
- "101056": "4.1.15",
- "101057": "4.1.15 Beta 1",
- "101058": "4.1.15 Beta 2",
- "101059": "4.1.15 Beta 2a",
- "101060": "4.1.15 Beta 3",
- "101061": "4.1.15",
- "101062": "4.1.15",
- "101063": "4.1.15",
- "101064": "4.1.16 Beta 1",
- "101065": "4.1.16 Beta 2",
- "101066": "4.1.16 Beta 3",
- "101067": "4.1.16 Beta 4",
- "101068": "4.1.16 Beta 5",
- "101069": "4.1.16",
- "101070": "4.1.16.1",
- "101071": "4.1.17 Beta 1",
- "101072": "4.1.17 Beta 1a",
- "101073": "4.1.17 Beta 1b",
- "101074": "4.1.17 Beta 2",
- "101075": "4.1.17 Beta 3",
- "101076": "4.1.17 Beta 3",
- "101077": "4.1.17",
- "101078": "4.1.17.1",
- "101079": "4.1.18 Beta 1",
- "101082": "4.1.18 Beta 1b",
- "101083": "4.1.18 Beta 2",
- "101084": "4.1.18 Beta 3",
- "101085": "4.1.18",
- "101088": "4.1.18.1",
- "101089": "4.1.18.2",
- "101090": "4.1.19 Beta 1",
- "101091": "4.1.19 Beta 2",
- "101092": "4.1.19 Beta 3",
- "101093": "4.1.19"
-}
\ No newline at end of file
diff --git a/applications/chat/data/widgets.json b/applications/chat/data/widgets.json
deleted file mode 100644
index 6ffe01cc..00000000
--- a/applications/chat/data/widgets.json
+++ /dev/null
@@ -1 +0,0 @@
-{"chatwhoschatting":{"class":"\\IPS\\Widget\\StaticCache","restrict":["sidebar","cms"],"default_area":"","allow_reuse":false,"menu_style":"menu","embeddable":true}}
\ No newline at end of file
diff --git a/applications/chat/extensions/core/FrontNavigation/Chat.php b/applications/chat/extensions/core/FrontNavigation/Chat.php
deleted file mode 100644
index 3dca9f8c..00000000
--- a/applications/chat/extensions/core/FrontNavigation/Chat.php
+++ /dev/null
@@ -1,76 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 8 Jan 2014
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\extensions\core\FrontNavigation;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * Front Navigation Extension: Chat
- */
-class _Chat extends \IPS\core\FrontNavigation\FrontNavigationAbstract
-{
- /**
- * Get Type Title which will display in the AdminCP Menu Manager
- *
- * @return string
- */
- public static function typeTitle()
- {
- return \IPS\Member::loggedIn()->language()->addToStack('frontnavigation_chat');
- }
-
- /**
- * Can the currently logged in user access the content this item links to?
- *
- * @return bool
- */
- public function canAccessContent()
- {
- return \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'chat', 'chat' ) );
- }
-
- /**
- * Get Title
- *
- * @return string
- */
- public function title()
- {
- return \IPS\Member::loggedIn()->language()->addToStack('frontnavigation_chat');
- }
-
- /**
- * Get Link
- *
- * @return \IPS\Http\Url
- */
- public function link()
- {
- return \IPS\Http\Url::internal( "app=chat&module=chat&controller=view", 'front', 'chat' );
- }
-
- /**
- * Is Active?
- *
- * @return bool
- */
- public function active()
- {
- return \IPS\Dispatcher::i()->application->directory === 'chat';
- }
-}
\ No newline at end of file
diff --git a/applications/chat/extensions/core/GroupForm/ChatAccess.php b/applications/chat/extensions/core/GroupForm/ChatAccess.php
deleted file mode 100644
index 82b2379f..00000000
--- a/applications/chat/extensions/core/GroupForm/ChatAccess.php
+++ /dev/null
@@ -1,54 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\extensions\core\GroupForm;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * Admin CP Group Form
- */
-class _ChatAccess
-{
- /**
- * Process Form
- *
- * @param \IPS\Helpers\Form $form The form
- * @param \IPS\Member\Group $group Existing Group
- * @return void
- */
- public function process( &$form, $group )
- {
- $form->add( new \IPS\Helpers\Form\YesNo( 'chat_access', $group->chat_access ) );
- $form->add( new \IPS\Helpers\Form\YesNo( 'chat_moderate', $group->chat_moderate ) );
- $form->add( new \IPS\Helpers\Form\YesNo( 'chat_private', $group->chat_private ) );
- }
-
- /**
- * Save
- *
- * @param array $values Values from form
- * @param \IPS\Member\Group $group The group
- * @return void
- */
- public function save( $values, &$group )
- {
- $group->chat_access = $values['chat_access'];
- $group->chat_moderate = $values['chat_moderate'];
- $group->chat_private = $values['chat_private'];
- }
-}
\ No newline at end of file
diff --git a/applications/chat/extensions/core/MemberForm/ChatBanned.php b/applications/chat/extensions/core/MemberForm/ChatBanned.php
deleted file mode 100644
index 8196b73b..00000000
--- a/applications/chat/extensions/core/MemberForm/ChatBanned.php
+++ /dev/null
@@ -1,50 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\extensions\core\MemberForm;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * Admin CP Member Form
- */
-class _ChatBanned
-{
- /**
- * Process Form
- *
- * @param \IPS\Helpers\Form $form The form
- * @param \IPS\Member $member Existing Member
- * @return void
- */
- public function process( &$form, $member )
- {
- $form->add( new \IPS\Helpers\Form\YesNo( 'chat_banned', !$member->chat_banned ) );
- }
-
- /**
- * Save
- *
- * @param array $values Values from form
- * @param \IPS\Member $member The member
- * @return void
- */
- public function save( $values, &$member )
- {
- $member->chat_banned = !$values['chat_banned'];
- }
-}
\ No newline at end of file
diff --git a/applications/chat/extensions/core/ModCpMemberManagement/ChatBanned.php b/applications/chat/extensions/core/ModCpMemberManagement/ChatBanned.php
deleted file mode 100644
index ddfebaf4..00000000
--- a/applications/chat/extensions/core/ModCpMemberManagement/ChatBanned.php
+++ /dev/null
@@ -1,104 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\extensions\core\ModCpMemberManagement;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * @brief Moderator Control Panel Member Management Extension: ChatBanned
- */
-class _ChatBanned
-{
- /**
- * Returns the tab key for the navigation bar
- *
- * @return string|null
- */
- public function getTab()
- {
- return 'chatBanned';
- }
-
- /**
- * Get content to display
- *
- * @return string
- */
- public function manage()
- {
- /* Create the table */
- $table = new \IPS\Helpers\Table\Db( 'core_members', \IPS\Http\Url::internal( 'app=core&module=modcp&tab=members&area=chat_banned' ), 'chat_banned=1' );
- $table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'modcp', 'core', 'front' ), 'memberManagementRow' );
-
- $table->langPrefix = 'members_';
-
- /* Columns we need */
- $table->include = array( 'member_id', 'name', 'email', 'joined', 'member_group_id', 'photo' );
- $table->mainColumn = 'name';
-
- /* Custom parsers */
- $table->parsers = array(
- 'joined' => function( $val, $row )
- {
- return \IPS\DateTime::ts( $val )->localeDate();
- },
- 'member_group_id' => function( $val, $row )
- {
- return \IPS\Member\Group::load( $val )->formattedName;
- },
- 'photo' => function( $val, $row )
- {
- return \IPS\Theme::i()->getTemplate( 'global', 'core' )->userPhoto( \IPS\Member::constructFromData( $row ), 'mini' );
- },
- );
-
- /* Individual member actions */
- $table->rowButtons = function( $row )
- {
- $member = \IPS\Member::constructFromData( $row );
-
- $return = array();
-
- $return['edit'] = array(
- 'icon' => 'pencil',
- 'title' => 'edit',
- 'link' => \IPS\Http\Url::internal( 'app=core&module=members&controller=profile&do=edit&id=' . $member->member_id, 'front', 'edit_profile', $member->members_seo_name )
- );
-
- $return['warn'] = array(
- 'icon' => '',
- 'title' => 'modcp_view_warnings',
- 'link' => \IPS\Http\Url::internal( 'app=core&module=system&controller=warnings&id=' . $member->member_id, 'front', 'warn_list', $member->members_seo_name )
- );
-
- $return['contact'] = array(
- 'icon' => 'envelope',
- 'title' => 'message',
- 'link' => \IPS\Http\Url::internal( 'app=core&module=messaging&controller=messenger&do=compose&to=' . $member->member_id, 'front', 'messenger_compose', $member->members_seo_name )
- );
-
- return $return;
- };
-
- /* Default sort options */
- $table->sortBy = $table->sortBy ?: 'joined';
- $table->sortDirection = $table->sortDirection ?: 'desc';
-
- return (string) $table;
- }
-}
\ No newline at end of file
diff --git a/applications/chat/hooks/tabCount.php b/applications/chat/hooks/tabCount.php
deleted file mode 100644
index 10cd2b21..00000000
--- a/applications/chat/hooks/tabCount.php
+++ /dev/null
@@ -1,62 +0,0 @@
-//chatters ) )
- {
- \IPS\Data\Store::i()->chatters = array();
- }
- $chatters = \IPS\Data\Store::i()->chatters;
- $seen = array();
-
- $count = 0;
-
- if( is_array( $chatters ) AND count( $chatters ) )
- {
- foreach( $chatters as $uid => $chatter )
- {
- /* Have we already seen this? Remove duplicates */
- if( in_array( $chatter['forumUserID'], $seen ) )
- {
- unset( $chatters[ $uid ] );
- }
-
- /* If the last update was longer than 120 seconds ago, they're gone */
- if( $chatter['last_update'] < time() - 120 )
- {
- unset( $chatters[ $uid ] );
- }
-
- $seen[ $chatter['forumUserID'] ] = $chatter['forumUserID'];
- }
-
- $count = count( $chatters );
- }
-
- if( !$count )
- {
- return;
- }
-
-\IPS\Output::i()->endBodyCode .= <<
- $('[data-role="navBarItem"][data-navApp="chat"][data-navExt="Chat"] > a').append( $(' ').addClass('ipsNotificationCount').text('{$count}') );
-
-HTML;
- }
-}
\ No newline at end of file
diff --git a/applications/chat/hooks/tabCountLoad.php b/applications/chat/hooks/tabCountLoad.php
deleted file mode 100644
index af3d17a1..00000000
--- a/applications/chat/hooks/tabCountLoad.php
+++ /dev/null
@@ -1,26 +0,0 @@
-//initLoad[] = 'chatters';
- }
- }
-
-}
\ No newline at end of file
diff --git a/applications/chat/hooks/tabNewWindow.php b/applications/chat/hooks/tabNewWindow.php
deleted file mode 100644
index 45e4dc90..00000000
--- a/applications/chat/hooks/tabNewWindow.php
+++ /dev/null
@@ -1,59 +0,0 @@
-//ipchat_new_window )
- {
- return;
- }
-
-\IPS\Output::i()->endBodyCode .= <<
- $('[data-role="navBarItem"][data-navapp="chat"][data-navext="Chat"] a').click( function( e ){
- e.preventDefault();
-
- var joinWith = '?';
-
- if ( $( e.currentTarget ).attr('href').indexOf('?') != -1 )
- {
- joinWith = '&';
- }
-
- window.open( $( e.currentTarget ).attr('href') + joinWith + '_popup=1', '_blank', "height=480,width=1200,location=0,menubar=0,toolbar=0" );
- return false;
- });
-
-HTML;
- }
-
- /**
- * Finish
- *
- * @return void
- */
- public function finish()
- {
- /* Is this a popup? */
- if ( \IPS\Request::i()->_popup == 1 AND \IPS\Dispatcher::i()->application->directory == 'chat' )
- {
- \IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->blankTemplate( \IPS\Output::i()->output ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
- }
-
- parent::finish();
- }
-}
\ No newline at end of file
diff --git a/applications/chat/interface/base64/base64.js b/applications/chat/interface/base64/base64.js
deleted file mode 100644
index bafc4820..00000000
--- a/applications/chat/interface/base64/base64.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(){function t(t){this.message=t}var r="undefined"!=typeof exports?exports:this,e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";t.prototype=new Error,t.prototype.name="InvalidCharacterError",r.btoa||(r.btoa=function(r){for(var o,n,a=String(r),i=0,c=e,d="";a.charAt(0|i)||(c="=",i%1);d+=c.charAt(63&o>>8-i%1*8)){if(n=a.charCodeAt(i+=.75),n>255)throw new t("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");o=o<<8|n}return d}),r.atob||(r.atob=function(r){var o=String(r).replace(/=+$/,"");if(o.length%4==1)throw new t("'atob' failed: The string to be decoded is not correctly encoded.");for(var n,a,i=0,c=0,d="";a=o.charAt(c++);~a&&(n=i%4?64*n+a:a,i++%4)?d+=String.fromCharCode(255&n>>(-2*i&6)):0)a=e.indexOf(a);return d})}();
\ No newline at end of file
diff --git a/applications/chat/interface/sounds/enter.mp3 b/applications/chat/interface/sounds/enter.mp3
deleted file mode 100644
index d3a4f76b..00000000
Binary files a/applications/chat/interface/sounds/enter.mp3 and /dev/null differ
diff --git a/applications/chat/interface/sounds/leave.mp3 b/applications/chat/interface/sounds/leave.mp3
deleted file mode 100644
index 49e22629..00000000
Binary files a/applications/chat/interface/sounds/leave.mp3 and /dev/null differ
diff --git a/applications/chat/interface/sounds/message.mp3 b/applications/chat/interface/sounds/message.mp3
deleted file mode 100644
index 07a88b80..00000000
Binary files a/applications/chat/interface/sounds/message.mp3 and /dev/null differ
diff --git a/applications/chat/modules/admin/chat/logs.php b/applications/chat/modules/admin/chat/logs.php
deleted file mode 100644
index 9c130f4f..00000000
--- a/applications/chat/modules/admin/chat/logs.php
+++ /dev/null
@@ -1,170 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\modules\admin\chat;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * logs
- */
-class _logs extends \IPS\Dispatcher\Controller
-{
- /**
- * Execute
- *
- * @return void
- */
- public function execute()
- {
- \IPS\Dispatcher::i()->checkAcpPermission( 'logs_manage' );
- parent::execute();
- }
-
- /**
- * Show logs
- *
- * @return void
- */
- protected function manage()
- {
- /* Create the table */
- $table = new \IPS\Helpers\Table\Db( 'chat_log_archive', \IPS\Http\Url::internal( 'app=chat&module=chat&controller=logs' ) );
- $table->langPrefix = 'chatlog_';
-
- /* Column stuff */
- $table->include = array( 'log_time', 'log_user', 'log_message' );
- $table->mainColumn = 'log_message';
- $table->widths = array( 'log_time' => '20', 'log_user' => '20', 'log_message' => '60' );
-
- /* Sort stuff */
- $table->sortBy = $table->sortBy ?: 'log_time';
- $table->sortDirection = $table->sortDirection ?: 'desc';
-
- /* Search */
- $table->quickSearch = 'log_message';
- $table->advancedSearch = array(
- 'log_user' => \IPS\Helpers\Table\SEARCH_CONTAINS_TEXT,
- 'log_time' => \IPS\Helpers\Table\SEARCH_DATE_RANGE,
- );
-
- /* Formatters */
- $self = $this;
-
- $table->parsers = array(
- 'log_time' => function( $val, $row )
- {
- $date = \IPS\DateTime::ts( $val );
-
- return $date->localeDate() . ' ' . $date->localeTime( FALSE );
- },
- 'log_user' => function( $val, $row ) use ( $self )
- {
- return $self->cleanMessage( $val );
- },
- 'log_message' => function( $val, $row ) use ( $self )
- {
- $val = $self->cleanMessage( $val );
-
- if( $row['log_code'] == 2 )
- {
- $_extra = explode( '_', $row['log_extra'] );
-
- if( $_extra[0] == 1 )
- {
- $val = \IPS\Member::loggedIn()->language()->addToStack('chatlog__hasentered', FALSE, array( 'sprintf' => $row['log_user'] ) );
- }
- else
- {
- $val = \IPS\Member::loggedIn()->language()->addToStack('chatlog__hasleft', FALSE, array( 'sprintf' => $row['log_user'] ) );
- }
- }
- else if( $row['log_code'] == 5 )
- {
- $val = \IPS\Member::loggedIn()->language()->addToStack('chatlog__kicked', FALSE, array( 'sprintf' => $val ) );
- }
-
- if( mb_strpos( $val, '/me' ) === 0 )
- {
- $val = mb_substr( $val, 4 );
- }
-
- if( mb_strpos( $row['log_extra'], 'private=' ) === 0 )
- {
- $_user = mb_substr( $row['log_extra'], mb_strrpos( $row['log_extra'], '=' ) + 1 );
- $_uid = $_user;
-
- $_user = \IPS\Member::load( $_user );
-
- $val = \IPS\Member::loggedIn()->language()->addToStack('chatlog__private', FALSE, array( 'sprintf' => array( $_user->name, $val ) ) );
- }
-
- return $val;
- }
- );
-
- /* Root buttons */
- \IPS\Output::i()->sidebar['actions']['settings'] = array(
- 'title' => 'prunesettings',
- 'icon' => 'cog',
- 'link' => \IPS\Http\Url::internal( 'app=chat&module=chat&controller=logs&do=settings' ),
- 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('prunesettings') )
- );
-
- /* Display */
- \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('ipschat_log_archive');
- \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core' )->block( 'title', (string) $table );
- }
-
- /**
- * Clean "special" characters
- *
- * @param string String to clean
- * @return string Cleaned string
- */
- public function cleanMessage( $string )
- {
- $string = str_replace( "__N__" , "\n", $string );
- $string = str_replace( "__C__" , ",", $string );
- $string = str_replace( "__E__" , "=", $string );
- $string = str_replace( "__PS__" , "+", $string );
- $string = str_replace( "__A__" , "&", $string );
- $string = str_replace( "__P__" , "%", $string );
-
- return $string;
- }
-
- /**
- * Settings
- *
- * @return void
- */
- public function settings()
- {
- $form = new \IPS\Helpers\Form;
- $form->add( new \IPS\Helpers\Form\Number( 'ipchat_log_prune', \IPS\Settings::i()->ipchat_log_prune, FALSE, array( 'unlimited' => 0, 'unlimitedLang' => 'never' ), NULL, \IPS\Member::loggedIn()->language()->addToStack('after'), \IPS\Member::loggedIn()->language()->addToStack('days') ) );
- if ( $form->values() )
- {
- $form->saveAsSettings();
-
- \IPS\Session::i()->log( 'acplog__chatlog_settings' );
- \IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=chat&module=chat&controller=logs' ), 'saved' );
- }
-
- \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'global', 'core' )->block( 'settings', $form, FALSE );
- }
-}
\ No newline at end of file
diff --git a/applications/chat/modules/admin/chat/settings.php b/applications/chat/modules/admin/chat/settings.php
deleted file mode 100644
index f0332ed8..00000000
--- a/applications/chat/modules/admin/chat/settings.php
+++ /dev/null
@@ -1,107 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\modules\admin\chat;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * settings
- */
-class _settings extends \IPS\Dispatcher\Controller
-{
- /**
- * Execute
- *
- * @return void
- */
- public function execute()
- {
- \IPS\Dispatcher::i()->checkAcpPermission( 'settings_manage' );
- parent::execute();
- }
-
- /**
- * Show the settings form
- *
- * @return void
- */
- protected function manage()
- {
- $form = new \IPS\Helpers\Form;
-
- $form->add( new \IPS\Helpers\Form\YesNo( 'ipchat_new_window', \IPS\Settings::i()->ipchat_new_window, TRUE ) );
- $form->add( new \IPS\Helpers\Form\YesNo( 'ipschat_enable_rules', \IPS\Settings::i()->ipschat_enable_rules, TRUE, array( 'togglesOn' => array( 'ipschat_rules_id' ) ) ) );
- $form->add( new \IPS\Helpers\Form\Translatable( 'ipschat_rules_text', NULL, FALSE, array( 'app' => 'core', 'key' => 'ipschat_rules', 'editor' => array( 'app' => 'core', 'key' => 'Admin', 'autoSaveKey' => 'ipschat_rules', 'attachIds' => array( NULL, NULL, 'ipschat_rules' ) ) ), NULL, NULL, NULL, 'ipschat_rules_id' ) );
-
- $form->add( new \IPS\Helpers\Form\YesNo( 'ipchat_hide_usermessage', !\IPS\Settings::i()->ipchat_hide_usermessage, TRUE ) );
- $form->add( new \IPS\Helpers\Form\YesNo( 'ipchat_no_buffer', \IPS\Settings::i()->ipchat_no_buffer, TRUE ) );
-
- $form->add( new \IPS\Helpers\Form\Number( 'ipchat_inactive_minutes', \IPS\Settings::i()->ipchat_inactive_minutes, TRUE, array(), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('minutes') ) );
- $form->add( new \IPS\Helpers\Form\Select( 'ipchat_24hour', \IPS\Settings::i()->ipchat_24hour, TRUE, array( 'options' => array( '12' => 'chat_12hour', '24' => 'chat_24hour' ) ) ) );
-
- if( \IPS\Settings::i()->ipschat_online )
- {
- $times = explode( ',', \IPS\Settings::i()->ipschat_online );
-
- /* Do we need to adjust the time zone? */
- if( isset( $times[2] ) AND $times[2] AND $times[2] != \IPS\Member::loggedIn()->timezone )
- {
- $start = new \IPS\DateTime( '2015-01-01 ' . $times[0], new \DateTimeZone( $times[2] ) );
- $start->setTimezone( new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
- $times[0] = $start->format( 'H:i' );
-
- $end = new \IPS\DateTime( '2015-01-01 ' . $times[1], new \DateTimeZone( $times[2] ) );
- $end->setTimezone( new \DateTimeZone( \IPS\Member::loggedIn()->timezone ) );
- $times[1] = $end->format( 'H:i' );
- }
- }
- else
- {
- $times = array( 0 => '', 1 => '' );
- }
-
- $form->add( new \IPS\Helpers\Form\Custom( 'ipschat_online', $times, FALSE, array(
- 'getHtml' => function( $element )
- {
- return \IPS\Theme::i()->getTemplate( 'settings' )->chatOnline( $element->value );
- },
- 'formatValue' => function( $element )
- {
- $element->value[2] = \IPS\Member::loggedIn()->timezone;
- return $element->value;
- }
- ), NULL, NULL, NULL, 'ipschat_online' ) );
-
- if ( $values = $form->values() )
- {
- \IPS\Lang::saveCustom( 'chat', "ipschat_rules", $values['ipschat_rules_text'] );
- unset( $values['ipschat_rules_text'] );
- $values['ipchat_hide_usermessage'] = !$values['ipchat_hide_usermessage'];
- $values['ipschat_online'] = implode( ',', $values['ipschat_online'] );
- $form->saveAsSettings( $values );
-
- /* Clear guest page caches */
- \IPS\Data\Cache::i()->clearAll();
-
- \IPS\Session::i()->log( 'acplogs__chat_settings' );
- }
-
- \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack('menu__chat_chat_settings');
- \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core' )->block( 'menu__chat_chat_settings', $form );
- }
-}
\ No newline at end of file
diff --git a/applications/chat/modules/admin/index.html b/applications/chat/modules/admin/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/modules/front/chat/action.php b/applications/chat/modules/front/chat/action.php
deleted file mode 100644
index eaa99cd5..00000000
--- a/applications/chat/modules/front/chat/action.php
+++ /dev/null
@@ -1,258 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 23 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\modules\front\chat;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * Action Controller
- */
-class _action extends \IPS\Content\Controller
-{
- /**
- * Init
- *
- * @return void
- */
- public function execute()
- {
- parent::execute();
- }
-
- /**
- * Add a user to the chat room
- *
- * @return void
- * @note Forum (local) user ID is in \IPS\Request::i()->id and Chat user ID is in \IPS\Request::i()->user
- */
- protected function addUser()
- {
- \IPS\Session::i()->csrfCheck();
-
- $member = \IPS\Member::load( intval( \IPS\Request::i()->id ) );
-
- $map = array(
- 'user_id' => intval( \IPS\Request::i()->user ),
- 'forumUserID' => intval( \IPS\Request::i()->id ),
- 'prefix' => $member->group['prefix'],
- 'suffix' => $member->group['suffix'],
- 'photo' => $member->photo,
- 'username' => $member->member_id ? $member->name : $member->loggedIn()->language()->get('guest'),
- 'formattedUsername' => \IPS\Member\Group::load( $member->member_group_id )->formatName( $member->name ),
- 'canBeIgnored' => !$member->canBeIgnored(),
- 'canBypassBadwords' => $member->group['g_bypass_badwords'],
- 'groupID' => $member->member_group_id,
- 'profileUrl' => $member->url(),
- 'canModerate' => $member->group['chat_moderate'],
- 'seo_name' => $member->members_seo_name,
- 'last_update' => time(),
- );
-
- /* Update the chatters list */
- $chatters = ( isset( \IPS\Data\Store::i()->chatters ) ) ? \IPS\Data\Store::i()->chatters : array();
- $chatters[ $map['user_id'] ] = $map;
-
- \IPS\Data\Store::i()->chatters = $chatters;
-
- /* Output */
- \IPS\Output::i()->json( array(
- 'html' => \IPS\Theme::i()->getTemplate( 'view' )->userRow( $map ),
- 'user' => $map
- ) );
- }
-
- /**
- * Load user info
- *
- * @return void
- * @note Forum (local) user ID is in \IPS\Request::i()->id
- */
- protected function loadUserInfo()
- {
- \IPS\Output::i()->sendOutput( \IPS\Theme::i()->getTemplate( 'view', 'chat' )->userInfo( \IPS\Member::load( intval( \IPS\Request::i()->id ) ) ), 200, 'text/html', \IPS\Output::i()->httpHeaders );
- }
-
- /**
- * Ignore and unignore chats
- *
- * @return void
- * @note Forum (local) user ID is in \IPS\Request::i()->id
- */
- protected function blockUser()
- {
- \IPS\Session::i()->csrfCheck();
-
- /* Make sure there is an ID */
- if( !\IPS\Request::i()->id )
- {
- \IPS\Output::i()->json( 'OK' );
- }
-
- /* Make sure we can ignore this user */
- $user = \IPS\Member::load( \IPS\Request::i()->id );
-
- if( !$user->canBeIgnored() )
- {
- \IPS\Output::i()->error( 'ipschat_no_ignore', '3H269/K', 403, '' );
- }
-
- /* Update or insert the ignore */
- try
- {
- $existing = \IPS\Db::i()->select( '*', 'core_ignored_users', array( 'ignore_owner_id=? AND ignore_ignore_id=?', \IPS\Member::loggedIn()->member_id, \IPS\Request::i()->id ) )->first();
-
- \IPS\Db::i()->update( 'core_ignored_users', array( 'ignore_chats' => $existing['ignore_chats'] ? 0 : 1 ), array( 'ignore_id=?', $existing['ignore_id'] ) );
- }
- catch( \UnderflowException $e )
- {
- \IPS\Db::i()->insert( 'core_ignored_users', array(
- 'ignore_owner_id' => \IPS\Member::loggedIn()->member_id,
- 'ignore_ignore_id' => intval( \IPS\Request::i()->id ),
- 'ignore_chats' => 1
- ) );
- }
-
- \IPS\Output::i()->json( 'OK' );
- }
-
- /**
- * Ban user
- *
- * @return void
- * @note Forum (local) user ID is in \IPS\Request::i()->id
- */
- protected function banUser()
- {
- \IPS\Session::i()->csrfCheck();
-
- /* Make sure there is an ID */
- if( !\IPS\Request::i()->id )
- {
- \IPS\Output::i()->json( 'OK' );
- }
-
- /* Make sure we can moderate */
- if( !\IPS\Member::loggedIn()->group['chat_moderate'] )
- {
- \IPS\Output::i()->error( 'ipschat_no_ban', '3H269/L', 403, '' );
- }
-
- /* Ban the user */
- $user = \IPS\Member::load( \IPS\Request::i()->id );
- $user->chat_banned = 1;
- $user->save();
-
- /* Remove us from chatters list */
- $chatters = ( isset( \IPS\Data\Store::i()->chatters ) ) ? \IPS\Data\Store::i()->chatters : array();
-
- foreach( $chatters as $uid => $chatter )
- {
- if( $chatter['forumUserID'] == $user->member_id )
- {
- unset( $chatters[ $uid ] );
- }
- }
-
- \IPS\Data\Store::i()->chatters = $chatters;
-
- \IPS\Output::i()->json( 'OK' );
- }
-
- /**
- * Ping to keep us in chatters list
- *
- * @return void
- * @note Forum (local) user ID is in \IPS\Request::i()->id
- */
- protected function ping()
- {
- /* Still online? */
- if ( $offlineMessage = \IPS\Application::load('chat')->offlineMessage() )
- {
- \IPS\Output::i()->json( array( 'message' => $offlineMessage ), 500 );
- }
-
- /* Make sure there is an ID */
- if( !\IPS\Request::i()->id )
- {
- \IPS\Output::i()->json( 'OK' );
- }
-
- /* If the data store doesn't exist it was wiped, so recreate it */
- if( !isset( \IPS\Data\Store::i()->chatters ) )
- {
- $chatters = array();
- $options = $_SESSION['chatOptions'];
-
- $result = \IPS\Http\Url::external( "http://{$options['serverHost']}{$options['serverPath']}/list.php?room={$options['roomId']}&user={$options['userId']}&access_key={$options['accessKey']}" )->request()->get();
-
- if( $result )
- {
- $results = explode( "~~||~~", $result );
- $status = array_shift( $results );
-
- if( $status == 1 )
- {
- foreach( $results as $k => $v )
- {
- $_thisRecord = explode( ',', $v );
-
- if( !$_thisRecord[0] )
- {
- continue;
- }
-
- $member = \IPS\Member::load( intval( $_thisRecord[2] ) );
- $username = str_replace( '~~#~~', ',', $_thisRecord[1] );
-
- $chatters[ $_thisRecord[0] ] = array(
- 'user_id' => $_thisRecord[0],
- 'username' => htmlspecialchars( $username, ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
- 'forumUserID' => $member->member_id,
- 'canBeIgnored' => !$member->canBeIgnored(),
- 'canBypassBadwords' => $member->group['g_bypass_badwords'],
- 'groupID' => $member->member_group_id,
- 'prefix' => str_replace( '"', '__DBQ__', $member->group['prefix'] ),
- 'suffix' => str_replace( '"', '__DBQ__', $member->group['suffix'] ),
- 'photo' => $member->photo,
- 'profileUrl' => $member->url(),
- 'canModerate' => $member->group['chat_moderate'],
- 'seo_name' => $member->members_seo_name,
- 'last_update' => time(),
- );
- }
- }
- }
- }
- else
- {
- $chatters = \IPS\Data\Store::i()->chatters;
- }
-
- /* Update us in chatters list - session will be auto-handled */
- if( isset( $chatters[ \IPS\Request::i()->id ] ) )
- {
- $chatters[ \IPS\Request::i()->id ]['last_update'] = time();
-
- \IPS\Data\Store::i()->chatters = $chatters;
- }
-
- /* Return a response */
- \IPS\Output::i()->json( 'OK' );
- }
-}
\ No newline at end of file
diff --git a/applications/chat/modules/front/chat/index.html b/applications/chat/modules/front/chat/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/modules/front/chat/view.php b/applications/chat/modules/front/chat/view.php
deleted file mode 100644
index c52045a9..00000000
--- a/applications/chat/modules/front/chat/view.php
+++ /dev/null
@@ -1,469 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 10 Oct 2013
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\modules\front\chat;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * View File Controller
- */
-class _view extends \IPS\Content\Controller
-{
- /**
- * @brief Master chat server URL
- */
- const MASTERSERVER = "http://chatservice.ipsdns.com/";
-
- /**
- * Init
- *
- * @return void
- */
- public function execute()
- {
- if ( time() > 1493596800 )
- {
- \IPS\Output::i()->error( 'ipschat_service_closed', '1H000/0', 503, 'ipschat_service_closed_admin' );
- }
-
- /* Check that the license allows access, etc. */
- $licenseData = \IPS\IPS::licenseKey();
-
- if( !$licenseData )
- {
- \IPS\Output::i()->error( 'ipschat_no_license', '4H268/1', 500, 'ipschat_no_license_admin' );
- }
-
- if( !isset( $licenseData['chat_limit'] ) OR $licenseData['chat_limit'] < 1 )
- {
- \IPS\Output::i()->error( 'ipschat_no_license', '4H269/1', 500, 'ipschat_no_license_access' );
- }
-
- /* Group access check */
- if( !\IPS\Member::loggedIn()->group['chat_access'] )
- {
- \IPS\Output::i()->error( 'ipschat_no_group_access', '2H269/2', 403, '' );
- }
-
- /* Is this a guest that was blocked? */
- if( !\IPS\Member::loggedIn()->member_id AND isset( \IPS\Request::i()->cookie['chat_blocked'] ) AND \IPS\Request::i()->cookie['chat_blocked'] )
- {
- \IPS\Output::i()->error( 'ipschat_no_access', '2H269/3', 403, '' );
- }
-
- /* Or a member? */
- if( \IPS\Member::loggedIn()->chat_banned )
- {
- \IPS\Output::i()->error( 'ipschat_no_access', '2H269/5', 403, '' );
- }
-
- /* Spiders also do not have access */
- if( \IPS\Session\Front::i()->userAgent->spider )
- {
- \IPS\Output::i()->error( 'ipschat_no_access', '2H269/4', 403, '' );
- }
-
- /* Figure out if chat is online due to time restrictions */
- if ( $offlineMessage = \IPS\Application::load('chat')->offlineMessage() )
- {
- \IPS\Output::i()->error( $offlineMessage, '1H269/H', 403, 'chat_offline_admin' );
- }
-
- parent::execute();
- }
-
- /**
- * View Chat
- *
- * @return void
- */
- protected function manage()
- {
- /* Initiate output */
- \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery.rangyinputs.js', 'core', 'interface' ) );
- \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'buzz/buzz.min.js', 'core', 'interface' ) );
- \IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'front_chat.js', 'chat', 'front' ) );
- \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'chat.css', 'chat', 'front' ) );
-
- if ( \IPS\Theme::i()->settings['responsive'] )
- {
- \IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'chat_responsive.css', 'chat', 'front' ) );
- }
-
- \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'chat_title' );
-
- /* Check rules */
- if( \IPS\Settings::i()->ipschat_enable_rules )
- {
- $proceed = FALSE;
-
- $form = new \IPS\Helpers\Form( 'chat_rules', 'accept_rules' );
- $form->class = 'ipsForm_vertical';
- $form->hiddenValues['proceed'] = TRUE;
-
- if( $values = $form->values() )
- {
- if( $values['proceed'] )
- {
- $proceed = TRUE;
- }
- }
-
- /* If we haven't agreed yet, show the form */
- if( !$proceed )
- {
- \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'view' )->acceptRules( $form );
- return;
- }
- }
-
- /* Initiate our options */
- $options = array(
- 'roomId' => 0,
- 'userId' => 0,
- 'userName' => $this->cleanUsername( \IPS\Member::loggedIn()->member_id ? \IPS\Member::loggedIn()->name : \IPS\Member::loggedIn()->language()->get('guest') ),
- 'accessKey' => '',
- 'serverHost' => '',
- 'serverPath' => '',
- );
-
- /* Call master to retrieve info */
- try
- {
- $result = \IPS\Http\Url::external( self::MASTERSERVER . "gateway31.php?api_key=" . \IPS\Settings::i()->ipb_reg_number . "&user={$options['userName']}&level=" . \IPS\Member::loggedIn()->group['chat_moderate'] )->request()->get();
- }
- catch ( \IPS\Http\Request\Exception $e )
- {
- \IPS\Output::i()->error( 'generic_chatconnect_error', '4H268/2', 500, 'connect_gw_error_0' );
- }
- $results = explode( ',', $result );
-
-
- /* Error? */
- if( $results[0] == 0 )
- {
- switch( $results[1] )
- {
- // No license key
- case 1:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/7', 500, 'connect_gw_error_1' );
- break;
-
- // No username
- case 2:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/8', 500, 'connect_gw_error_2' );
- break;
-
- // MySQL error
- case 3:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H268/3', 500, 'connect_gw_error_3' );
- break;
-
- // Invalid room ID
- case 5:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/A', 500, 'connect_gw_error_5' );
- break;
-
- // Chat room full
- case 6:
- $key = \IPS\IPS::licenseKey();
- \IPS\Output::i()->error( 'chat_room_full', ( $key['chat_limit'] ? '1' : '4' ) . 'H269/B', 403, $key['chat_limit'] ? 'license_no_chat' : NULL );
- break;
-
- // The chat room is on a server which is no longer online
- case 7:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/M', 500, 'connect_gw_error_7' );
- break;
-
- // The chat room has no hostname associated with the server it is on; this would be a misconfiguration issue on the chat master server
- case 8:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H268/4', 500, 'connect_gw_error_8' );
- break;
-
- // No online chat servers could be found
- case 10:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H268/5', 500, 'connect_gw_error_10' );
- break;
-
- // Other
- default:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/6', 500 );
- break;
- }
- }
-
- /* Set the options we just got from the master chat server */
- $options['roomId'] = intval($results[3]);
- $options['serverHost'] = $results[1];
- $options['serverPath'] = $results[2];
-
- /* If this is a guest, adjust the URL to send an identifier */
- $guestFlag = ( !\IPS\Member::loggedIn()->member_id and isset( \IPS\Request::i()->cookie['chat_user_id'] ) ) ? "&userid=" . intval( \IPS\Request::i()->cookie['chat_user_id'] ) : '';
-
- /* Now join the actual chat room */
- try
- {
- $result = \IPS\Http\Url::external( "http://{$options['serverHost']}{$options['serverPath']}/join31.php?api_key=" . \IPS\Settings::i()->ipb_reg_number . "&user={$options['userName']}&level=" . \IPS\Member::loggedIn()->group['chat_moderate'] . "&room={$options['roomId']}&forumid=" . \IPS\Member::loggedIn()->member_id . "{$guestFlag}&forumgroup=" . \IPS\Member::loggedIn()->member_group_id )->request()->get();
- }
- catch ( \IPS\Http\Request\Exception $e )
- {
- \IPS\Output::i()->error( 'generic_chatconnect_error', '4H269/N', 500, 'connect_gw_error_0' );
- }
- $results = explode( ',', $result );
-
- /* Error? */
- if( $results[0] == 0 )
- {
- switch( $results[1] )
- {
- // No license key
- case 1:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/C', 500, 'connect_gw_error_1' );
- break;
-
- // No username
- case 2:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/D', 500, 'connect_gw_error_2' );
- break;
-
- // No room ID
- case 3:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/E', 500, 'connect_gw_error_3' );
- break;
-
- // MySQL error
- case 4:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/E', 500, 'connect_gw_error_4' );
- break;
-
- // Could not create room
- case 5:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/F', 500, 'connect_gw_error_5' );
- break;
-
- // License key mismatch
- case 6:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H269/G', 500, 'connect_gw_error_6' );
- break;
-
- // Chat room full
- case 7:
- $key = \IPS\IPS::licenseKey();
- \IPS\Output::i()->error( 'chat_room_full', ( $key['chat_limit'] ? '1' : '4' ) . 'H269/H', 500, $key['chat_limit'] ? 'license_no_chat' : NULL );
- break;
-
- // Other
- default:
- \IPS\Output::i()->error( 'generic_chatconnect_error', '5H268/6', 500, 'connect_gw_error_0' );
- break;
- }
- }
-
- /* Remember the important parts of this response */
- $options['userId'] = $results[1];
- $options['accessKey'] = $results[2];
-
- /* If this is a guest, set the chat user ID cookie */
- if( !\IPS\Member::loggedIn()->member_id )
- {
- \IPS\Request::i()->setCookie( 'chat_user_id', $options['userId'], NULL, FALSE );
- }
-
- /* Fetch the online users list and build the map */
- $chatters = array();
-
- $result = \IPS\Http\Url::external( "http://{$options['serverHost']}{$options['serverPath']}/list.php?room={$options['roomId']}&user={$options['userId']}&access_key={$options['accessKey']}" )->request()->get();
-
- if( $result )
- {
- $results = explode( "~~||~~", $result );
- $status = array_shift( $results );
-
- if( $status == 1 )
- {
- foreach( $results as $k => $v )
- {
- $_thisRecord = explode( ',', $v );
-
- if( !$_thisRecord[0] )
- {
- continue;
- }
-
- $member = \IPS\Member::load( intval( $_thisRecord[2] ) );
- $username = str_replace( '~~#~~', ',', $_thisRecord[1] );
-
- $chatters[ $username ] = array(
- 'user_id' => $_thisRecord[0],
- 'username' => htmlspecialchars( $this->uncleanUsername( $username ), ENT_QUOTES | \IPS\HTMLENTITIES, 'UTF-8', FALSE ),
- 'formattedUsername' => \IPS\Member\Group::load( $member->member_group_id )->formatName( $member->name ),
- 'forumUserID' => $member->member_id,
- 'canBeIgnored' => !$member->canBeIgnored(),
- 'canBypassBadwords' => $member->group['g_bypass_badwords'],
- 'groupID' => $member->member_group_id,
- 'prefix' => str_replace( '"', '__DBQ__', $member->group['prefix'] ),
- 'suffix' => str_replace( '"', '__DBQ__', $member->group['suffix'] ),
- 'photo' => $member->photo,
- 'profileUrl' => $member->url(),
- 'canModerate' => $member->group['chat_moderate'],
- 'seo_name' => $member->members_seo_name,
- 'last_update' => time(),
- );
- }
- }
- }
-
- ksort($chatters);
-
- $forumIdMap = array();
-
- foreach( $chatters as $chatter )
- {
- $forumIdMap[ $chatter['user_id'] ] = $chatter;
- }
-
- /* Update the chatters list */
- if( count( $chatters ) )
- {
- \IPS\Data\Store::i()->chatters = $forumIdMap;
- }
-
- /* Get badwords list */
- $badwords = iterator_to_array( \IPS\Db::i()->select( 'm_exact, swop, type', 'core_profanity_filters' )->setKeyField( 'type' ) );
-
- /* Get groups */
- $groups = array();
-
- foreach( \IPS\Member\Group::groups() as $group )
- {
- $groups[ $group->g_id ] = array( 'prefix' => $group->prefix, 'suffix' => $group->suffix, 'bypassBadwords' => $group->g_bypass_badwords );
- }
-
- /* Are we ignoring any chats? */
- if( \IPS\Member::loggedIn()->member_id )
- {
- $ignored = iterator_to_array( \IPS\Db::i()->select( 'ignore_ignore_id, ignore_id', 'core_ignored_users', array( 'ignore_owner_id=? and ignore_chats=1', \IPS\Member::loggedIn()->member_id ) )->setKeyField( 'ignore_ignore_id' )->setValueField( 'ignore_id' ) );
- }
- else
- {
- $ignored = array();
- }
-
- /* Emoticons */
- $emoticons = array();
- foreach ( \IPS\Db::i()->select( '*', 'core_emoticons', NULL, 'emo_set,emo_position' ) as $row )
- {
- $emoticons[ $row['typed'] ] = array( 'image' => (string) \IPS\File::get( 'core_Emoticons', $row['image'] )->url, 'width' => $row['width'], 'height' => $row['height'] );
-
- if( $row['image_2x'] )
- {
- $emoticons[ $row['typed'] ]['image_2x'] = (string) \IPS\File::get( 'core_Emoticons', $row['image_2x'] )->url . ' 2x';
- }
- }
-
- /* We store the chat options in our session in case we choose to leave the room */
- $_SESSION['chatOptions'] = $options;
-
- /* Display */
- \IPS\Output::i()->sidebar['enabled'] = FALSE;
- \IPS\Output::i()->output = \IPS\Theme::i()->getTemplate( 'view' )->chat( $forumIdMap );
- \IPS\Output::i()->endBodyCode .= \IPS\Theme::i()->getTemplate( 'view' )->chatvars( $options, $forumIdMap, $groups, $badwords, $ignored, $emoticons );
- }
-
- /**
- * Leave Chat
- *
- * @return void
- */
- protected function leave()
- {
- /* Check secure key */
- \IPS\Session::i()->csrfCheck();
-
- /* Call server */
- if( isset( $_SESSION['chatOptions'] ) )
- {
- $options = $_SESSION['chatOptions'];
-
- \IPS\Http\Url::external( "http://{$options['serverHost']}{$options['serverPath']}/leave.php?room={$options['roomId']}&user={$options['userId']}&access_key={$options['accessKey']}" )->request()->get();
-
- unset( $_SESSION['chatOptions'] );
- }
-
- /* Remove us from chatters list */
- $chatters = ( isset( \IPS\Data\Store::i()->chatters ) ) ? \IPS\Data\Store::i()->chatters : array();
-
- foreach( $chatters as $uid => $chatter )
- {
- if( $chatter['forumUserID'] == \IPS\Member::loggedIn()->member_id )
- {
- unset( $chatters[ $uid ] );
- }
- }
-
- \IPS\Data\Store::i()->chatters = $chatters;
-
- // If this is a popup, close it, otherwise send us back to the suite index
- if( \IPS\Request::i()->_popup == 1 )
- {
- \IPS\Output::i()->output = "";
- }
- else
- {
- \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "_leaveChat=1" ), 'leave_chat_room' );
- }
- }
-
- /**
- * Clean username for chat
- *
- * @param string Username
- * @return string
- */
- protected function cleanUsername( $username )
- {
- $username = str_replace( "\r", '', $username );
- $username = str_replace( "\n", '__N__', $username );
- $username = str_replace( ",", '__C__', $username );
- $username = str_replace( "=", '__E__', $username );
- $username = str_replace( "+", '__PS__', $username );
- $username = str_replace( "&", '__A__', $username );
- $username = str_replace( "%", '__P__', $username );
- $username = urlencode($username);
-
- return $username;
- }
-
- /**
- * Unclean username for chat
- *
- * @param string Username
- * @return string
- */
- protected function uncleanUsername( $username )
- {
- $username = str_replace( "__PS__", "+", $username );
- $username = str_replace( "__E__", "=", $username );
- $username = str_replace( "__P__", "%", $username );
- $username = str_replace( "__A__", "&", $username );
- $username = str_replace( "__C__", ",", $username );
- $username = str_replace( "__N__", "\n", $username );
-
- return $username;
- }
-}
\ No newline at end of file
diff --git a/applications/chat/modules/front/index.html b/applications/chat/modules/front/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/modules/index.html b/applications/chat/modules/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/setup/index.html b/applications/chat/setup/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/setup/install/index.html b/applications/chat/setup/install/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/setup/install/queries.json b/applications/chat/setup/install/queries.json
deleted file mode 100644
index d6b6dac8..00000000
--- a/applications/chat/setup/install/queries.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "1": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_access",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can access chat?",
- "unsigned": true
- }
- ]
- },
- "2": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_moderate",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can moderate chat?",
- "unsigned": true
- }
- ]
- },
- "3": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_private",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can send private chats?",
- "unsigned": true
- }
- ]
- },
- "4": {
- "method": "addColumn",
- "params": [
- "core_members",
- {
- "name": "chat_banned",
- "type": "TINYINT",
- "length": 1,
- "null": false,
- "default": 0,
- "comment": "",
- "unsigned": true
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/applications/chat/setup/upg_100021/index.html b/applications/chat/setup/upg_100021/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/setup/upg_100021/queries.json b/applications/chat/setup/upg_100021/queries.json
deleted file mode 100644
index ea93090a..00000000
--- a/applications/chat/setup/upg_100021/queries.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "1": {
- "method": "addIndex",
- "params": [
- "chat_log_archive",
- {
- "type": "key",
- "name": "log_time",
- "columns": [
- "log_time"
- ],
- "length": [
- null
- ]
- }
- ]
- },
- "2": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_access",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can access chat?",
- "unsigned": true
- }
- ]
- },
- "3": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_moderate",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can moderate chat?",
- "unsigned": true
- }
- ]
- },
- "4": {
- "method": "addColumn",
- "params": [
- "core_groups",
- {
- "name": "chat_private",
- "type": "TINYINT",
- "length": 10,
- "null": false,
- "default": 0,
- "comment": "Can send private chats?",
- "unsigned": true
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/applications/chat/setup/upg_100021/upgrade.php b/applications/chat/setup/upg_100021/upgrade.php
deleted file mode 100644
index f15673dc..00000000
--- a/applications/chat/setup/upg_100021/upgrade.php
+++ /dev/null
@@ -1,82 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\setup\upg_100021;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * 4.0.0 RC 5 Upgrade Code
- */
-class _Upgrade
-{
- /**
- * Step 1
- * Rebuild reputation received
- *
- * @return array If returns TRUE, upgrader will proceed to next step. If it returns any other value, it will set this as the value of the 'extra' GET parameter and rerun this step (useful for loops)
- */
- public function step1()
- {
- /* Set access permission */
- if( \IPS\Settings::i()->ipschat_group_access )
- {
- \IPS\Db::i()->update( 'core_groups', array( 'chat_access' => 1 ), 'g_id IN(' . \IPS\Settings::i()->ipschat_group_access . ')' );
- }
- else
- {
- \IPS\Db::i()->update( 'core_groups', array( 'chat_access' => 1 ) );
- }
-
- if( \IPS\Settings::i()->ipschat_mods )
- {
- \IPS\Db::i()->update( 'core_groups', array( 'chat_moderate' => 1 ), 'g_id IN(' . \IPS\Settings::i()->ipschat_mods . ')' );
- }
-
- if( \IPS\Settings::i()->ipschat_private )
- {
- \IPS\Db::i()->update( 'core_groups', array( 'chat_private' => 1 ), 'g_id IN(' . \IPS\Settings::i()->ipschat_private . ')' );
- }
-
- /* Add rules text */
- \IPS\Lang::saveCustom( 'core', "ipschat_rules", \IPS\Text\Parser::utf8mb4SafeDecode( \IPS\Settings::i()->ipschat_rules ) );
-
- /* Add online/offline start value */
- \IPS\Db::i()->replace( 'core_sys_conf_settings', array(
- 'conf_key' => 'ipschat_online',
- 'conf_default' => '',
- 'conf_value' => implode( ',', array( 0 => \IPS\Settings::i()->ipschat_online_start, 1 => \IPS\Settings::i()->ipschat_online_end ) ),
- 'conf_app' => 'chat',
- ) );
-
- /* Remove old settings */
- \IPS\Db::i()->delete( 'core_sys_conf_settings', "conf_key IN ('ipschat_group_access', 'ipschat_mods', 'ipschat_private',
- 'ipschat_account_key', 'ipchat_htc_zero', 'ipchat_max_messages', 'ipschat_offline_msg', 'ipschat_offline_groups', 'ipschat_hide_chatting',
- 'ipschat_whos_chatting', 'ipchat_htc_view', 'ipschat_format_names', 'ipschat_online_start', 'ipschat_online_end', 'ipschat_rules')" );
- return TRUE;
- }
-
- /**
- * Custom title for this step
- *
- * @return string
- */
- public function step1CustomTitle()
- {
- return "Converting and cleaning up settings";
- }
-}
\ No newline at end of file
diff --git a/applications/chat/setup/upg_11005/index.html b/applications/chat/setup/upg_11005/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/setup/upg_11005/queries.json b/applications/chat/setup/upg_11005/queries.json
deleted file mode 100644
index adb14906..00000000
--- a/applications/chat/setup/upg_11005/queries.json
+++ /dev/null
@@ -1,119 +0,0 @@
-{
- "1": {
- "method": "createTable",
- "params": [
- {
- "name": "chat_log_archive",
- "columns": {
- "log_id": {
- "allow_null": false,
- "auto_increment": true,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": null,
- "length": 10,
- "name": "log_id",
- "type": "INT",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_room_id": {
- "allow_null": false,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": "0",
- "length": 10,
- "name": "log_room_id",
- "type": "INT",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_time": {
- "allow_null": false,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": "0",
- "length": 10,
- "name": "log_time",
- "type": "INT",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_code": {
- "allow_null": false,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": "0",
- "length": 6,
- "name": "log_code",
- "type": "SMALLINT",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_user": {
- "allow_null": true,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": null,
- "length": 255,
- "name": "log_user",
- "type": "VARCHAR",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_message": {
- "allow_null": true,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": null,
- "length": null,
- "name": "log_message",
- "type": "TEXT",
- "unsigned": false,
- "values": [],
- "zerofill": false
- },
- "log_extra": {
- "allow_null": true,
- "auto_increment": false,
- "binary": false,
- "comment": "",
- "decimals": null,
- "default": null,
- "length": 255,
- "name": "log_extra",
- "type": "VARCHAR",
- "unsigned": false,
- "values": [],
- "zerofill": false
- }
- },
- "indexes": {
- "PRIMARY": {
- "type": "primary",
- "name": "PRIMARY",
- "columns": [
- "log_id"
- ]
- }
- }
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/applications/chat/setup/upg_40000/index.html b/applications/chat/setup/upg_40000/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/sources/index.html b/applications/chat/sources/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/tasks/chatlogprune.php b/applications/chat/tasks/chatlogprune.php
deleted file mode 100644
index 94eddd60..00000000
--- a/applications/chat/tasks/chatlogprune.php
+++ /dev/null
@@ -1,61 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\tasks;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * chatlogprune Task
- */
-class _chatlogprune extends \IPS\Task
-{
- /**
- * Execute
- *
- * If ran successfully, should return anything worth logging. Only log something
- * worth mentioning (don't log "task ran successfully"). Return NULL (actual NULL, not '' or 0) to not log (which will be most cases).
- * If an error occurs which means the task could not finish running, throw an \IPS\Task\Exception - do not log an error as a normal log.
- * Tasks should execute within the time of a normal HTTP request.
- *
- * @return mixed Message to log or NULL
- * @throws \IPS\Task\Exception
- */
- public function execute()
- {
- if( \IPS\Settings::i()->ipchat_log_prune )
- {
- \IPS\Db::i()->delete( 'chat_log_archive', array( 'log_time < ?', \IPS\DateTime::create()->sub( new \DateInterval( 'P' . \IPS\Settings::i()->ipchat_log_prune . 'D' ) )->getTimestamp() ) );
- }
-
- return NULL;
- }
-
- /**
- * Cleanup
- *
- * If your task takes longer than 15 minutes to run, this method
- * will be called before execute(). Use it to clean up anything which
- * may not have been done
- *
- * @return void
- */
- public function cleanup()
- {
-
- }
-}
\ No newline at end of file
diff --git a/applications/chat/tasks/chatlogrefresh.php b/applications/chat/tasks/chatlogrefresh.php
deleted file mode 100644
index 7c272773..00000000
--- a/applications/chat/tasks/chatlogrefresh.php
+++ /dev/null
@@ -1,108 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\tasks;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * chatlogrefresh Task
- */
-class _chatlogrefresh extends \IPS\Task
-{
- /**
- * Execute
- *
- * If ran successfully, should return anything worth logging. Only log something
- * worth mentioning (don't log "task ran successfully"). Return NULL (actual NULL, not '' or 0) to not log (which will be most cases).
- * If an error occurs which means the task could not finish running, throw an \IPS\Task\Exception - do not log an error as a normal log.
- * Tasks should execute within the time of a normal HTTP request.
- *
- * @return mixed Message to log or NULL
- * @throws \IPS\Task\Exception
- */
- public function execute()
- {
- if ( !\IPS\Application::appIsEnabled( 'chat' ) )
- {
- return NULL;
- }
-
- if ( !\IPS\Settings::i()->ipb_reg_number )
- {
- throw new \IPS\Task\Exception( $this, 'ipschat_no_license_admin' );
- }
-
- /* Get last log time */
- $max = \IPS\Db::i()->select( 'MAX(log_time)', 'chat_log_archive' )->first();
-
- /* Fetch logs */
- try
- {
- $result = \IPS\Http\Url::external( \IPS\chat\modules\front\chat\view::MASTERSERVER . "get_logs.php?api_key=" . \IPS\Settings::i()->ipb_reg_number . "×tamp=" . $max )->request()->get();
- }
- catch( \IPS\Http\Request\Exception $e )
- {
- throw new \IPS\Task\Exception( $this, $e->getMessage() );
- }
-
- $results = explode( ',', $result );
- $status = array_shift( $results );
-
- if( $status == 0 )
- {
- throw new \IPS\Task\Exception( $this, 'connect_gw_error_' . $results[0] );
- }
-
- foreach( $results as $k => $v )
- {
- $_thisResult = explode( "~~||~~", $v );
-
- if( !isset( $_thisResult[1] ) OR !$_thisResult[1] )
- {
- continue;
- }
-
- $_insert = array(
- 'log_room_id' => $_thisResult[1],
- 'log_time' => $_thisResult[2],
- 'log_code' => $_thisResult[3],
- 'log_user' => $_thisResult[4],
- 'log_message' => $_thisResult[5],
- 'log_extra' => $_thisResult[6],
- );
-
- \IPS\Db::i()->insert( "chat_log_archive", $_insert );
- }
-
- return NULL;
- }
-
- /**
- * Cleanup
- *
- * If your task takes longer than 15 minutes to run, this method
- * will be called before execute(). Use it to clean up anything which
- * may not have been done
- *
- * @return void
- */
- public function cleanup()
- {
-
- }
-}
\ No newline at end of file
diff --git a/applications/chat/tasks/index.html b/applications/chat/tasks/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/chat/widgets/chatwhoschatting.php b/applications/chat/widgets/chatwhoschatting.php
deleted file mode 100644
index 200f4bc8..00000000
--- a/applications/chat/widgets/chatwhoschatting.php
+++ /dev/null
@@ -1,78 +0,0 @@
-Invision Power Services, Inc.
- * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
- * @license http://www.invisionpower.com/legal/standards/
- * @package IPS Community Suite
- * @subpackage Chat
- * @since 15 Mar 2015
- * @version SVN_VERSION_NUMBER
- */
-
-namespace IPS\chat\widgets;
-
-/* To prevent PHP errors (extending class does not exist) revealing path */
-if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
-{
- header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
- exit;
-}
-
-/**
- * chatwhoschatting Widget
- */
-class _chatwhoschatting extends \IPS\Widget\StaticCache
-{
- /**
- * @brief Widget Key
- */
- public $key = 'chatwhoschatting';
-
- /**
- * @brief App
- */
- public $app = 'chat';
-
- /**
- * @brief Plugin
- */
- public $plugin = '';
-
- /**
- * Render a widget
- *
- * @return string
- */
- public function render()
- {
- if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'chat', 'chat' ) ) )
- {
- return '';
- }
-
- $chatters = ( isset( \IPS\Data\Store::i()->chatters ) ) ? \IPS\Data\Store::i()->chatters : array();
-
- /* It's possible to get duplicates from the chat server if a user rejoins the room, so weed them out */
- $seen = array();
-
- foreach( $chatters as $uid => $chatter )
- {
- /* Have we already seen this? Remove duplicates */
- if( in_array( $chatter['forumUserID'], $seen ) )
- {
- unset( $chatters[ $uid ] );
- }
-
- /* If the last update was longer than 120 seconds ago, they're gone */
- if( $chatter['last_update'] < time() - 120 )
- {
- unset( $chatters[ $uid ] );
- }
-
- $seen[ $chatter['forumUserID'] ] = $chatter['forumUserID'];
- }
-
- return $this->output( $chatters );
- }
-}
\ No newline at end of file
diff --git a/applications/chat/widgets/index.html b/applications/chat/widgets/index.html
deleted file mode 100644
index e69de29b..00000000
diff --git a/applications/cms/Application.php b/applications/cms/Application.php
index 4c8fff51..7f1093ec 100644
--- a/applications/cms/Application.php
+++ b/applications/cms/Application.php
@@ -342,18 +342,18 @@ class _Application extends \IPS\Application
) );
/* Needs to be added before createDatabase is called */
- \IPS\Lang::saveCustom( 'cms', "content_db_" . $database->id, "Статьи" );
- \IPS\Lang::saveCustom( 'cms', "module__cms_records" . $database->id, "Статьи" );
- \IPS\Lang::saveCustom( 'cms', "content_db_" . $database->id . '_desc', "Статьи нашего сайта" );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_sl_" . $database->id, 'статья' );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_pl_" . $database->id, 'статьи' );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_su_" . $database->id, 'Статья' );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_pu_" . $database->id, 'Статьи' );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_ia_" . $database->id, 'статья' );
- \IPS\Lang::saveCustom( 'cms', "content_db_lang_sl_" . $database->id . '_pl', 'Статьи' );
- \IPS\Lang::saveCustom( 'cms', "cms_create_menu_records_" . $database->id, 'Запись в Статьи' );
- \IPS\Lang::saveCustom( 'cms', "digest_area_records" . $database->id, "Статьи" );
- \IPS\Lang::saveCustom( 'cms', "cms_records" . $database->id . '_pl', 'Статья' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_" . $database->id, "Articles" );
+ \IPS\Lang::saveCustom( 'cms', "module__cms_records" . $database->id, "Articles" );
+ \IPS\Lang::saveCustom( 'cms', "content_db_" . $database->id . '_desc', "Our website articles" );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_sl_" . $database->id, 'article' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_pl_" . $database->id, 'articles' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_su_" . $database->id, 'Article' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_pu_" . $database->id, 'Articles' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_ia_" . $database->id, 'an article' );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_sl_" . $database->id . '_pl', 'Articles' );
+ \IPS\Lang::saveCustom( 'cms', "cms_create_menu_records_" . $database->id, 'Article in Articles' );
+ \IPS\Lang::saveCustom( 'cms', "digest_area_cms_records" . $database->id, "Articles" );
+ \IPS\Lang::saveCustom( 'cms', "cms_records" . $database->id . '_pl', 'Article' );
try
{
@@ -389,13 +389,15 @@ class _Application extends \IPS\Application
$link = (string) \IPS\Http\Url::ips('docs/pages_docs');
$content = <<Добро пожаловать в приложение Страницы!
-Приложение Страницы призвано расширить ваш сайт путём гибкого управления контентом, разработанное специально для сообществ.
-Создайте совершенно новые разделы вашего сообщества, используя такие особенности, как блоки, базы данных и статьи, и внедряя дополнительную информацию из других разделов сообщества.
-Создавайте собственные страницы, используя наш удобный WYSIWYG редактор.
-Создавайте блоки, которые отображают все виды информации из всего вашего сообщества для создания динамических страниц, или используйте стандартные готовые блоки, присутствующие в IPS Community Suite.
+Welcome to Pages!
+Pages extends your site with custom content management designed especially for communities.
+Create brand new sections of your community using features like blocks, databases and articles,
+pulling in data from other areas of your community.
+Create custom pages in your community using our drag'n'drop, WYSIWYG editor.
+Build blocks that pull in all kinds of data from throughout your community to create dynamic pages,
+or use one of the ready-made widgets we include with the IPS Community Suite.
-Просмотреть документацию приложения
+View our Pages documentation
EOF;
$titleField = 'field_' . $database->field_title;
@@ -405,7 +407,7 @@ EOF;
$member = \IPS\Member::loggedIn()->member_id ? \IPS\Member::loggedIn() : \IPS\Member::load(1);
$record = $item::createItem( $member, \IPS\Request::i()->ipAddress(), \IPS\DateTime::ts( time() ), $category, FALSE );
- $record->$titleField = "Добро пожаловать в Страницы";
+ $record->$titleField = "Welcome to Pages";
$record->$contentField = $content;
$record->save();
@@ -416,8 +418,8 @@ EOF;
/* Create the page */
$pageValues = array(
- 'page_name' => "Статьи",
- 'page_title' => "Статьи",
+ 'page_name' => "Articles",
+ 'page_title' => "Articles",
'page_seo_name' => "articles.html",
'page_folder_id' => 0,
'page_ipb_wrapper' => 1,
@@ -453,12 +455,12 @@ EOF;
$buttonUrl = (string) \IPS\Http\Url::internal( 'applications/cms/interface/default/block_arrow.png', 'none' );
$defaultContent = <<
- Добро пожаловать в приложение Страницы!
+ Welcome to Pages!
- Для начала, убедитесь, что вы авторизовались и нажмите на стрелку с правой стороны , чтобы развернуть менеджер блоков.
+ To get started, make sure you are logged in and click the arrow on the left hand side to expand the block manager.
- Вы можете перемещать, добавлять и редактировать блоки без необходимости программирования!
+ You can move, add and edit blocks without the need for complex coding!
EOF;
@@ -488,7 +490,7 @@ EOF;
/* Add block container (custom)*/
$container = new \IPS\cms\Blocks\Container;
$container->parent_id = 0;
- $container->name = "Дополнительно";
+ $container->name = "Custom";
$container->type = 'block';
$container->key = 'block_custom';
$container->save();
@@ -496,7 +498,7 @@ EOF;
/* Add block container (plugins) */
$container = new \IPS\cms\Blocks\Container;
$container->parent_id = 0;
- $container->name = "Плагины";
+ $container->name = "Plugins";
$container->type = 'block';
$container->key = 'block_plugins';
$container->save();
diff --git a/applications/cms/data/acpsearch.json b/applications/cms/data/acpsearch.json
index 259446f9..fc548e17 100644
--- a/applications/cms/data/acpsearch.json
+++ b/applications/cms/data/acpsearch.json
@@ -3,9 +3,7 @@
"lang_key": "menu__cms_databases_categories",
"restriction": null,
"keywords": [
- "Articles Categories",
- "категории статей",
- "категории записей"
+ "Articles Categories"
]
},
"app=cms&module=databases&controller=databases": {
@@ -13,9 +11,7 @@
"restriction": "databases_manage",
"keywords": [
"database",
- "databases",
- "база данных",
- "базы данных"
+ "databases"
]
},
"app=cms&module=databases&controller=records&do=manage": {
@@ -23,20 +19,14 @@
"restriction": null,
"keywords": [
"article",
- "articles",
- "запись",
- "статья",
- "записи",
- "статьи"
+ "articles"
]
},
"app=cms&module=pages&controller=blocks": {
"lang_key": "menu__cms_pages_blocks",
"restriction": "block_manage",
"keywords": [
- "blocks",
- "блоки",
- "блок"
+ "blocks"
]
},
"app=cms&module=pages&controller=media": {
@@ -44,9 +34,7 @@
"restriction": "media_manage",
"keywords": [
"media",
- "media manager",
- "медиа",
- "медиа менеджер"
+ "media manager"
]
},
"app=cms&module=pages&controller=pages": {
@@ -55,19 +43,14 @@
"keywords": [
"add page",
"articles",
- "pages",
- "статьи",
- "страницы",
- "добавить страницу"
+ "pages"
]
},
"app=cms&module=pages&controller=templates": {
"lang_key": "menu__cms_pages_templates",
"restriction": "template_manage",
"keywords": [
- "templates",
- "шаблоны",
- "шаблон"
+ "templates"
]
}
}
\ No newline at end of file
diff --git a/applications/cms/data/javascript.xml b/applications/cms/data/javascript.xml
index 4075491a..f34aebb6 100644
--- a/applications/cms/data/javascript.xml
+++ b/applications/cms/data/javascript.xml
@@ -1,6 +1,6 @@
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2014 Invision Power Services - http://www.invisionpower.com
*
@@ -187,7 +187,7 @@
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2014 Invision Power Services - http://www.invisionpower.com
*
@@ -250,7 +250,7 @@
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -422,7 +422,7 @@
}
});
}(jQuery, _));
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2014 Invision Power Services - http://www.invisionpower.com
*
@@ -1296,7 +1296,7 @@
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -1346,7 +1346,7 @@
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -1418,7 +1418,7 @@
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -2642,7 +2642,7 @@
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -2680,7 +2680,7 @@
}
});
}(jQuery, _));
-
-
- \
{{{contents}}}\
\
@@ -2874,7 +2874,7 @@ ips.templates.set( 'templates.media.noSearchResults', "\
{{#lang}}mediaNoResults{{/lang}}\
\
");]]>
- \
diff --git a/applications/cms/data/lang.xml b/applications/cms/data/lang.xml
index 7a8a3ffb..09d6ba94 100644
--- a/applications/cms/data/lang.xml
+++ b/applications/cms/data/lang.xml
@@ -1,6 +1,6 @@
-
+
Pages
Pages
book
diff --git a/applications/cms/data/lang_rus.xml b/applications/cms/data/lang_rus.xml
deleted file mode 100644
index d69a4be6..00000000
--- a/applications/cms/data/lang_rus.xml
+++ /dev/null
@@ -1,942 +0,0 @@
-
-
-
- Pages
- Pages
- book
- a %s
- a comment
- a review
- %s
- comment
- review
- Rebuilding %s categories
- Rebuilding %s reciprocal maps (field ID %s)
- Rebuilding %s records
- Rebuilding %s synchronized topics
- No permission
- Converting bbcode pages to HTML
- Database Records
- Pages
- Number of records to include
- Records priority
- The priority value in a sitemap file indicates to search engines how important a page is compared to other pages on the site.
- Number of pages to include
- Pages priority
- The priority value in a sitemap file indicates to search engines how important a page is compared to other pages on the site.
- Can pin %s?
- Can unpin %s?
- Can feature %s?
- Can unfeature %s?
- Can edit %s?
- Can hide %s?
- Can unhide %s?
- Can view a hidden %s?
- Can move %s?
- Can lock %s?
- Can unlock %s?
- Can reply to a locked %s?
- Can delete %s?
- Can split and merge %s?
- Can future publish %s
- Can view a future %s
- Can edit a %s comment?
- Can hide a %s comment?
- Can unhide a %s comment?
- Can view a hidden %s comment?
- Can delete a %s comment?
- Can edit a %s review?
- Can hide a %s review?
- Can unhide a %s review?
- Can view a hidden %s review?
- Can delete a %s review?
- Can create pages?
- Can edit pages?
- Can delete pages?
- Pages
- Can manage page permissions?
- Can view blocks?
- Can create blocks?
- Can edit blocks?
- Can delete blocks?
- Blocks
- Can view blocks?
- Can manage block permissions?
- Can create/edit templates?
- Can delete templates?
- Templates
- Can view templates?
- Can create databases?
- Can edit databases?
- Can delete databases?
- Databases
- Can view databases?
- Can create records?
- Can edit records?
- Can delete records?
- Records
- Can view records?
- Extra Fields
- Can view extra fields?
- Can create extra fields?
- Can edit extra fields?
- Can delete extra fields?
- Custom Blocks
- CMS Pages
- CMS Widgets
- CMS Records
- CMS Records
- CMS Pages
- CMS Media
- Databases
- Can Manage Revisions
- Can manage meta tags
-
- Can Manage Record Slugs
- Record slugs are part of the FURL. This allows a custom slug to be set.
- Can See All Records
- Show all records in categories that are set to only show the authors own records
- Categories
- Page Management
- Pages
- Menu
- Blocks
- Templates
- Content
- Databases
- Records
- Categories
- Fields
- Pages
- Databases
- Create New Category
- Create New Block
- Edit Block
- Editing Block %s
- Name
- Parent
- Type
- Plugin
- You can use one of the listed plugins to create the output.
- Custom
- You can author HTML or PHP for advertisements, javascript widgets (e.g. stock tickers, gamer feeds, etc.) and notices/announcements.
- Editor
- Manual HTML
- Allows use of HTML and IPS4 template logic that you'll enter manually
- Manual PHP
-
- Content Editor
- Details
- Content
- Feed Configuration
- Name
- Description
- Category
- Content
- You cannot include a {block} tag for a block inside of itself.
- Feed
- Select a plugin
- You have chosen to create a new plugin block. Please select which plugin to use below.
- You have chosen to create a new feed block. Please select which feed to use below.
- Add Feed Block
- Content Type
- Template
- Template
- Custom
- Default template
-
- Use this template as-is
- Use as a base for a custom template
- You are creating a new block using the data feed from %s.
- Cannot locate the data feed required for this block.
- View Template
- Save this as a template
- You can use this content for this block, or you can save it as a template so that others can use it
- Template name
- Could not locate that template
- Refresh Preview
- Database Feeds
- Feed from %s
- Cannot create a feed for %s as this database is not assigned to a page
- Cannot use database '%s' as this database is not assigned to a page
- Templates
- Template Key
- The block key ('my_block_key') can be used in HTML templates and manual pages to fetch a specific block, for instance: {block="my_block_key"} The key must be unique for this database. If omitted, a key will be automatically generated.]]>
- The block key is not unique
- Cache this block
- If your block contains dynamic elements, such as member data, database counts, etc, you will need to prevent this block from being cached.
- Records
- Block Plugin Templates
- Page Templates
- Page CSS
- Page JS
- Database Templates
- Select a template to load
- Variables
- Description
- Name
- Modified
- Custom
- block template]]>
- page template]]>
- database template]]>
- CSS file]]>
- Javascript file]]>
- Add block template
- Add page template
- Add database template
- Add CSS file
- Add Javascript file
- Name
- Parameters
- Description
- Category
- Category
- Type
- Content
- Feed
- Plugin
- Theme set
- Type
- Record Listing (Shown when clicking a category)
- Featured Records (First page when viewing a database set to show featured articles)
- Category List (First page when viewing a database)
- Record (Displays the record and comments)
- Record Form (Used when adding/editing a record)
- Template Type
- Import Developer Templates
-
- View Block
- JS
- HTML
- Assign to database
- None
- Existing Group
- New Group
- Please use only a-z and 0-9 without any special characters or spaces. The group name must start with a letter or underscore.
- Please use only a-z and 0-9 without any special characters or spaces. The title must start with a letter or underscore.
- Switch off designers' mode. ]]>
- Download Templates
- Select the templates to export from the list below. These templates will be added to a single XML file that can be imported to other IPS4 communities.
- Blocks
- Database
- Pages
- You must select templates to export
- Upload Templates
- Upload
- Templates Imported
- Database template group options
- Group Name
- Please use only a-z and 0-9 without any special characters or spaces. The group name must start with a letter or underscore. You can use an underscore, which will show as a space when viewing templates.
- This template is used by: %s
- Contains database: %s
- Edit Folder
- Add Folder
- Add Page
- Edit Page
- Editing Page %s
- Folder Name
- Folder Parent
- This folder name is already in use. Please choose another name.
- This page file name is already in use. Please choose another name.
-
- View Page
- Details
-
- Content
- Page Name
- Page Title
- This value will be used in the document's HTML title tag. If omitted, the page name will be used instead.
- Page Filename
- Page Folder
- Meta Keywords
- Meta Description
- Show the suite sidebar on this page
- Content Editor
- Page Builder
- A visual editor for drag and drop page creation
- Manual HTML
- A code level editor for complete control
- Use suite HTML wrapper
- Outputs your page within the suite header/footer wrapper. If you select 'No', you will be responsible for the full page html (including doctype, head, body, etc. tags)
- Could not load a page to edit
- Editing page content for %s
- Content
- Make default page for this folder
- Page
- You may optionally create a page for this database. After creation, you may edit the page as normal.
- Database page
- I will add this database into an existing page
- I want to create a new page for this database
- Page could not be saved: %s
- This database was never added to any pages
- A page may only have one database embedded
- That database does not exist.
- The database added is being used by another page.
- You can edit this page's content by visiting the page once it has been created.
- page.]]>
- Open page builder
- View page
- Theme
- Select which theme to use for this page. The theme is used for default widgets and the global template.
- Default Theme
- Template
- Select which template to use for the page builder. Changing this for an existing page may lose any existing widget layouts.
- View Page
- Used on page: %s
- This folder name cannot be used as it causes a URL collision with another installed application.
- This page name cannot be used as it causes a URL collision with a folder in the same location.
- This page name cannot be used as it causes a URL collision with a friendly URL from another application.
- This folder name cannot be used as it causes a URL collision with a page in the same location.
- Page Includes
- Javascript
- CSS
- {template="includeCSS" app="core" group="global" location="global" params=""} {template="includeJS" app="core" group="global" location="global" params=""} ]]>
- Page Wrapper
- Wrapper template for this page
- None
- The syntax of tags or HTML logic is badly formatted.
- Advanced Settings
- Use a gateway under your suite root?
- More information]]>
- URL to gateway file
- The URL could not be found.
- Your IPS 4 suite is already installed at the root of your domain.
- Could not load Pages in this location. Have you uploaded the 'index.php' file?
- More information]]>
- download this file , rename it to '.htaccess' (with no file extension) and upload it to the same location as the special 'index.php' for Pages. If there is already a .htaccess file, add the contents of this one to the existing. If you are not sure how to do this, contact technical support for assistance.]]>
- Blocks
- Databases
- Pages
- Error Page
- A custom error page will be shown when the suite displays an error. Page builder pages are not suitable for error pages.
- Do not use a custom error page
- Error Message
- Error Code
- Homepage Options
- Move pages to root level (no folder)
- Move pages to
- Page
- Menu
- Add to front navigation menu
- Title
- Use the page name.
- I will enter a different title.
- Title
- You may leave this blank to use the page's title.
- Parent
- Title
- Description
- Type
- Content
- Custom Input Validation
- Unique Field
- This field can only accept unique values per database when this setting is enabled.
- This value is being used in another %s
- None
- Custom
- Email Address
- Integer
- URL
- Apply text formatter
- Lower-case all letters
- Capitalize all letters
- Capitalize the first letter
- Capitalize the first letter of each word
- Remove excess punctuation
- Format Numerically
- Regex
- Custom Error
- Max. Length
- Formatting Options
- Show in listing template
- The listing template is used when showing a list of records inside a category
- Show in display template
- The display template is used when showing a record
- Topic Format
- This setting controls whether the field will be included in the automatically generated topic, and how to format it. Use {title} for field title, and {value} for field value. Leave empty to not include in the automatically generated topic.
- Display options
- Truncate in listing
- Number of lines to truncate field content at in listing template.
- Field Required
- Accept HTML
- When enabled, users with permission to edit this field will be able to post raw HTML. Allowing users to post HTML is extremely dangerous and malicious users can use the feature to damage your site. It is strongly recommended that this feature is not enabled at all, or only for trusted staff groups
- Show Map
- It is not recommended that you show a map on the listing page without a custom template due to space in the default templates
- Show Map
- Map Size
- Map Size
- View Field Content
- Edit Field Content
- Add Content to Field
- Groups
- Default Value
- Allow filtering
- When enabled, you may select this option to filter by when viewing a list of records.
- Allow searching
- Title
- Content
- Publish Fields
- Publish Date
- This field will only be available for those with 'Can set a future publishing date' moderator permission.
- Expiry Date
- Allow Comments/Reviews
- Comment Cut Off Date
- Record Image
- Member Groups
- View Field Content
- Edit Field Content
- Add Content to Field
- Visible
- Hidden
- The value entered is incorrect
- Form Fields
- Use Fields
- The designated title and content fields are always shown on display and on the form.
- Editing field %s
- Add field
- Editable when viewing a record
- This allows the field value to be changed without editing the record.
- No value]]>
- Set as record title field
- Set as record content field
- Cannot locate that custom field
- Label
- Value
- Display View Format
- Custom
- Display Custom Format
- For example, your dropdown may use a key of 'open' with a value of 'Open Record'. In this example, {$formValue} would contain 'open' and {$value} would contain 'Open Record'. You may need to add the '|raw' modifier to preserve HTML, i.e. {$value|raw}, but this can introduce security issues if not used carefully. You may use IPS4 template logic such as {{if}}. If using an Address field, you can show a map by using {map} and the address with {address}. If using mulitple uploads, $value is an array of File Objects.]]>
- Listing View Format
- Listing Custom Format
- For example, your dropdown may use a key of 'open' with a value of 'Open Record'. You may need to add the '|raw' modifier to preserve HTML, i.e. {$value|raw}, but this can introduce security issues if not used carefully. You may use IPS4 template logic such as {{if}}. If using an Address field, you can show a map by using {map} and the address with {address}. If using mulitple uploads, $value is an array of File Objects.]]>
- Show to the right of the listing area
- The HTML syntax for the custom format is incorrect.
- Template Key
- The field key can be used in HTML templates to fetch a specific field, for instance: {$record->customFieldDisplayByKey('my_key', 'listing')|raw} The key must be unique for this database. If omitted, a key will be automatically generated. Valid values for the second parameter include 'listing', 'display', 'raw', and 'processed'.]]>
- The field key is not unique
- General Options
- Settings
- No formatting
- YouTube
- Database Relationship
- In listing, show as
- In a record, display as
- Embedded Player
- Plain URL
- Parameters
- You can add any specific player parameters here. If you do not supply 'width' and 'height', defaults will be used.
- Spotify
- Soundcloud
- Youtube URL
- Spotify URL
- Soundcloud URL
- Start typing the title of a %s
- Upload images only
- Upload any files
- Max. image dimensions
- Any image larger that these dimensions will be resized automatically
- Upload type
- The custom field type %s does not exist, please select a valid field type.
- {$record->customFieldDisplayByKey('title_key', ['display|listing'])|raw}. Using $record->_title will always get the article formatted title.]]>
- {$record->customFieldDisplayByKey('content_key', ['display|listing'])|raw}. Using $record->_content will always get the article formatted content.]]>
- Timezone
- Allow time input
- Timezone
- Update existing records
- This will update existing records with the current default value to the new default value
- Allow multiple uploads
- Database
- Cross link records?
- Enabling this option will show the records in this database related to a given record in the remote database
- There are no databases to select
- You must select a database
- Display
- This may not have any affect when using custom templates
- Above the item content
- Under the item content
- Allow decimals
- Number of decimal places
- UTC
- Manually select a timezone
- Use the viewing member's timezone
- Configuration
- %s Categories
- The FURL slug is not unique
- Manually enter Friendly URL slug?
- FURL Slug
- Parent
- Show Records in Listing?
- Set Custom Permissions
- When enabled, you can select member group permissions independently of the parent database
- Users can see records posted by other users?
- Records posted in a category that doesn't allow others to view records will not show in any sidebar, header or footer blocks.]]>
- Records posted in a database that doesn't allow others to view records will not show in any sidebar, header or footer blocks.]]>
- If you select 'no', only subcategories will be displayed in this category. If you select 'yes', subcategories and records will both be displayed.
-
- Meta Keywords
- Meta Description
- Forum
- Override database forum options
- Category Title
- This value will be used in the document's HTML title tag. If omitted, the page name will be used instead.
- Enable Star Ratings?
- Display
- Use database selected template
- Listing Template
- Display Template
- This category name cannot be used as it is a reserved word.
- Category Tree
- %s Records
- Title
- ID
- Date
- Category
- Status
- Unapproved
- Approved
- Unapproved
- Hidden
- Pinned
- Any Category
- Has Comments?
- Yes or No
- Yes
- No
- Content
- Category
- Tags
- Publish
- Publish Settings
- Status
- Unapproved
- Approved
- Hidden
- Publish date
- Expiry date
- After this date, the article will only display in the archives. Leave blank for no expiration.
- Author
- I am the author
- Choose a member
- Guest
- Author's Name
- Comments
- Allow comments
- Comments locked
- Leave blank to not set a cutoff date
- Pin record
- Meta Tags
- Meta Keywords
- Meta Description
- No date specified
- Allowed file extensions
- Any extensions
- Multiple Selection
- Deleted
- Set custom URL
- %spage/{page}/{category}/
- By default, the URL for this item will be /record-title-r{record_id}. You can set a custom URL that is different from the record title
- Custom URL
- This custom URL is in use already. Please choose a unique URL.
- Image
- Reason for edit
- Show that the message has been edited
- Staff may still be able to see this
- (show revisions)
- Feature this %s
- Unfeature this %s
- Pin this %s
- Unpin this %s
- Hide this %s
- Unhide this %s
- Approve this %s
- Lock this %s
- Unlock this %s
- Move this %s
- Delete this %s
- Select Category
- Name
- Name
- Categories
- Records
- Manage Records
- Permissions
- Manage Categories
- Manage Fields
- %s Fields
- Details
- Options
- Language
- Page title
- Determine which to use when using this database on a page
- Use database name as page title
- Use page title
- Name
- Description
- Category Template Group
- Listing Template Group
- Display Template Group
- Singular Lowercase Noun
- Default is record
- Plural Lowercase Noun
- Default is records
- Singular Capitalized Noun
- Default is Record
- Plural Capitalized Noun
- Default is Records
- Indefinite Article
- Default is 'a record'
- Wiki-style editing
- any user to edit any non-locked item, similar to a wiki. When not enabled, users can only edit their own non-locked item (with appropriate permissions).]]>
- Edit own records indefinitely
- When not enabled, the member group content editing permissions are used.]]>
- Store revisions
- When users or admins edit the items in this database, a copy of the original item will be stored which can be restored or compared later
- Allow searching
- Update the last action
- The Last Action shows in the category view and is how items are sorted by default/can be used to sort items.
- When the item is edited or a new comment is made
- When the item is edited only
- When a new comment is made only
- Enable RSS feed
- Custom RSS Feed]]>
- Number of items to show
- Can use tags?
- Can set tag prefixes?
- Defined Tags
- Use default
- If using a closed system, users will have to choose from these tags. If using an open system, tags entered here will show as suggestions.
- Field Options
- Sort By
- Order By
- Sort
- Order
- Records per page
- Record ID
- Member ID
- Submission Date
- Last Updated Date
- Last Edited Date
- Rating
- Views
- Approved
- Comments
- Locked
- Ascending (0-9)
- Descending (9-0)
- Forums
- Post topic
- Post a topic to the specific forum each time a new record is added.
- Rebuild synchronized topic content.]]>
- Use forum for comments
- Comments will be posted as replies. If you change the way comments are stored, your existing comments will no longer be displayed. Comments are not copied to the forums or to the internal comments database if you change this setting.
- Forum
- Topic prefix
- Topic suffix
- Delete topic
- Topic will be deleted automatically if the record is deleted
- Database does not exist
-
- See Database
- View Records
- Add Records
- Edit Records
- Add Comments
- Rate Records
-
- See Category
- View Records
- Add Records
- Edit Records
- Add Comments
- Rate Records
- Review Records
- record
- Record
- records
- Records
- a record
- Database creation failed: %s
- Database index
- Show categories]]>
- Show records like articles
- This displays records in an article or blog format showing more of the record content.
- Articles template
- Add/Edit Form Template
- Records to show
- Allow pagination
- If there are more featured records than the 'Records to show' value, then pagination links will show if enabled
- Sort by
- Order
- Download Database
- Records are not included.]]>
- Add Database
- Create New Database
- Upload Database
- Create a new custom database for this community.
- Database .xml file
- Importing %s
- Database Settings
- Database Permissions
- Use categories
- You can optionally not use categories which means that the default home page for this database is the record list, and the category name is not added to the URL.
- You have %s categories in this database. You can only have 1 category to store records in the database directly.
- Yes, allow records to be stored in different categories]]>
- No, store records in the database directly]]>
- Categories not used]]>
- Show featured records only
- Allow comments
- Comments must be approved?
- Allow reviews
- Reviews must be approved?
- Comments and Reviews
- Add Reviews
- Any categories
- Categories
- Templates
- Database Key
- The database key can be used in HTML templates and manual pages to fetch a specific database, for instance: {database="my_database_key"} The key must be unique for this database. If omitted, a key will be automatically generated.]]>
- The database key is not unique
- Editing Database %s
- Adding Database
- Record Image Settings
- Max. image dimensions
- Thumbnail dimensions
- The record image field is currently disabled.
- Database does not exist
- Moderate new records
- Enable record image upload
- This adds an image upload field to the record form. The image is displayed within the record text area by default.
- These settings will have no affect until this database is added to a page.
- You have post topic enabled but have not selected a forum.
- Promote from Forums
- Enable
- Add to category
- Available for
- This tool will add a background task to resychronize the content generated in the first post of a topic created by this database. It will also create topics for records if a topic does not exist.
- Continue
- Rebuild task added to the queue.
- Media
- Media
- View Media
- Add Media
- Edit Media
- Delete Media
- New Folder
- Upload New Media
- Delete Selected
- Search media...
- Folders
- File Overview
- Pages Media
- Media Tag
- Insert this tag in your pages/templates to insert this media item.
- Name
- Parent
- Preview
- Could not locate that media item
- Filename
- Uploaded
- File Size
- Dimensions
- Parent
-
- The current URL for this item is: %s.]]>
- Delete Folder
- Categories
- Subcategories
- %d %s
- %d %s in this category
- No %s here yet
- {!# [1:comment][?:comments]}
- {!# [1:review][?:reviews]}
- {!# [1:view][?:views]}
- By %s
- Actions
- Report %s
- Add a comment...
- Submit Comment
- Comment on this %s
- Notify me of replies
- Create new %s
- Status
- Locked
- Pinned
- Hidden
- Featured
- Save
- Create New %s
- %s Comment
- %s Comments
- %s comments
- %s Review
- %s Reviews
- %s reviews
- Type
- All
- Open
- Locked
- This record is locked
- Last comment date
- Number of comments
- Number of views
- Title
- Publish Date
- Editing %s
- Revisions
- No revisions to show
- All Revisions for '%s'
-
- Delete
- Revision
- Current
- Revision from %s for '%s'
- Restore from this revision
- Keep the current version
- Restore
- Restored record '%s' with revision #%s
- View full %s
- Mark as read
- Add new %s
- Update %s
-
- Filtered by:
- Clear filters
- Filters cleared
- There are no categories to show
- There are no %s to show
- This has not been read
- %s, ]]>
- Show all categories
- Show featured %s
- Your rating
- Your review
- Add a review...
- {# [1:Review][?:Reviews]}
- {# [1:Comment][?:Comments]}
- Comments
- Comment by %s
- Added by %s
- Updated by %s
- Go to %s
- Go to %s
- Go to the next unread %s
- Next unread %s
- %s to %s ]]>
-
- This %s is locked, but your permissions allow you to add new replies.
- Select a category
- %s in %s
- Add a comment recording this change.
- %s Information
- No changes recorded
- This is now closed for further comments
- Moderator Options
- Database '%s' is already used by page '%s'
- %s linking to this %s
- %s to %s
- From %s
- To %s
- Database
- This cannot be changed. If you want to create a feed from a different database, please create a new custom block.
- This cannot be changed. If you want to create a feed from a different database, please create a new custom block.]]>
- Categories
- All categories
- Record status
- Open
- Closed
- Pinned
- Not Pinned
- Visible
- Hidden
- Featured
- Not Featured
- Record started by
- Minimum record comments
- any
- Records to show
- Sort by
- Last comment
- Title
- Number of comments
- Publish date
- Record views
- Direction
- Descending (9-0)
- Ascending (0-9)
- Record Feed
- Create a customizable feed of database records
- Plain HTML Editor
- Adds entered text as HTML
- WYSIWYG Editor
- Rich text editor
- Database
- Adds a database to the page
- Latest Records
- Show latest articles from a database
- Please select a database for this widget.
- Latest %s
- Database Filters
- %s Filters
- Show updateable filters when viewing a database category
- Database Category Menu
- Displays a list of categories for the database used on the page
- No databases are available to be added.
-
-
- Custom Blocks
- Select one of the custom blocks you have created
- No custom blocks are available to be added.
- Custom Block
- Database
- RSS Import
- Import from an RSS feed
- Title
- RSS URL
- Number of items to show
- Cache for
- minutes
- Categories
- Viewing %s
- Viewing %s category %s
- Viewing %s in category %s: %s
- Viewing %s in %s
- Cannot find the page you requested
- You do not have permission to view this page
- This topic is used for %s comments and cannot be deleted
- %s
- %s posted a %s in %s
- %s commented on a %s
- Added Database: %s
- Edited Database: %s
- Deleted Database: %s
- External embed
- External Embed
- isn't part of your IPS Community Suite. For pages created within the IPS Community Suite, use the normal embedding method instead.]]>
- Embedding this block
- To embed this block, paste the following code in your page where you'd like it to display. You can optionally have the block automatically inherit some styles (font family, link color, etc.) from the page it appears on.
- Inherit key styles from the parent page
- Insert the loader
- once per page, even if you use many external blocks. It should be inserted either in your <head> tag, or just before the </body> tag.]]>
- View the %s %s
- Rebuilding Database Comment Likes
- Last Comment
- Comments
- Views
- Publish Date
- Database Comments
- Database Records
- Database Reviews
- Download Database
- Records are not included.]]>
- Show categories
- Show record list
- No media items selected
- {# [1:media item selected][?:media items selected]}
- There are no items in this folder yet
- Are you sure you want to delete {!# [1:this file][?:# selected files]}?
- There was an error deleting {!# [1:this file][?:these files]}
- No files match your search term
- {!# [1:A file was][?:# files were]} added to this folder
-
-
diff --git a/applications/cms/data/versions.json b/applications/cms/data/versions.json
index 2406124d..7b4c92c3 100644
--- a/applications/cms/data/versions.json
+++ b/applications/cms/data/versions.json
@@ -179,5 +179,11 @@
"101090": "4.1.19 Beta 1",
"101091": "4.1.19 Beta 2",
"101092": "4.1.19 Beta 3",
- "101093": "4.1.19"
+ "101093": "4.1.19",
+ "101094": "4.1.19.1",
+ "101095": "4.1.19.2 Beta 1",
+ "101096": "4.1.19.2",
+ "101097": "4.1.19.2",
+ "101098": "4.1.19.3",
+ "101099": "4.1.19.4"
}
\ No newline at end of file
diff --git a/applications/cms/sources/Fields/Fields.php b/applications/cms/sources/Fields/Fields.php
index 9b4b04a6..aeebf074 100644
--- a/applications/cms/sources/Fields/Fields.php
+++ b/applications/cms/sources/Fields/Fields.php
@@ -1281,7 +1281,7 @@ class _Fields extends \IPS\CustomField implements \IPS\Node\Permissions
return implode( ', ', array_map( function( $id )
{
return \IPS\Member::load( $id )->link();
- }, explode( ',', $value ) ) );
+ }, explode( "\n", $value ) ) );
}
break;
case 'Url':
@@ -2851,7 +2851,7 @@ class _Fields extends \IPS\CustomField implements \IPS\Node\Permissions
$value = array_map( function( $id )
{
return \IPS\Member::load( $id );
- }, explode( ',', $value ) );
+ }, explode( "\n", $value ) );
}
break;
case 'Date':
diff --git a/applications/convert/Application.php b/applications/convert/Application.php
new file mode 100644
index 00000000..1ccf14c0
--- /dev/null
+++ b/applications/convert/Application.php
@@ -0,0 +1,250 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Converter Application Class
+ */
+class _Application extends \IPS\Application
+{
+ /**
+ * [Node] Get Icon for tree
+ *
+ * @note Return the class for the icon (e.g. 'globe')
+ * @return string|null
+ */
+ protected function get__icon()
+ {
+ return 'random';
+ }
+
+ /**
+ * Install Other
+ *
+ * @return void
+ */
+ public function installOther()
+ {
+ static::checkConvParent();
+
+ try
+ {
+ \IPS\Db::i()->select( '*', 'core_login_handlers', array( "login_key=?", 'Convert' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(login_order)', 'core_login_handlers' )->first();
+ \IPS\Db::i()->insert( 'core_login_handlers', array(
+ 'login_key' => "Convert",
+ 'login_enabled' => 1,
+ 'login_settings' => json_encode( array( 'auth_types' => \IPS\Login::AUTH_TYPE_USERNAME + \IPS\Login::AUTH_TYPE_EMAIL ) ),
+ 'login_order' => $position + 1,
+ 'login_acp' => 1
+ ) );
+
+ if ( isset( \IPS\Data\Store::i()->loginHandlers ) )
+ {
+ unset( \IPS\Data\Store::i()->loginHandlers );
+ }
+ }
+ }
+
+ /**
+ * Ensure the appropriate tables have a conv_parent column for internal references
+ *
+ * @param string|NULL The application to check, or NULL to check all.
+ * @return void
+ */
+ public static function checkConvParent( $application=NULL )
+ {
+ $parents = array(
+ 'downloads' => array(
+ 'tables' => array(
+ 'downloads_categories' => array(
+ 'prefix' => 'c',
+ 'column' => 'conv_parent'
+ )
+ )
+ ),
+ 'forums' => array(
+ 'tables' => array(
+ 'forums_forums' => array(
+ 'prefix' => '',
+ 'column' => 'conv_parent'
+ )
+ )
+ ),
+ 'gallery' => array(
+ 'tables' => array(
+ 'gallery_categories' => array(
+ 'prefix' => 'category_',
+ 'column' => 'conv_parent'
+ )
+ )
+ ),
+ 'cms' => array(
+ 'tables' => array(
+ 'cms_containers' => array(
+ 'prefix' => 'container_',
+ 'column' => 'conv_parent'
+ ),
+ 'cms_database_categories' => array(
+ 'prefix' => 'category_',
+ 'column' => 'conv_parent'
+ ),
+ 'cms_folders' => array(
+ 'prefix' => 'folder_',
+ 'column' => 'conv_parent'
+ )
+ )
+ ),
+ 'nexus' => array(
+ 'tables' => array(
+ 'nexus_alternate_contacts' => array(
+ 'prefix' => '',
+ 'column' => 'conv_alt_id',
+ ),
+ 'nexus_package_groups' => array(
+ 'prefix' => 'pg_',
+ 'column' => 'conv_parent'
+ ),
+ 'nexus_packages' => array(
+ 'prefix' => 'p_',
+ 'column' => 'conv_associable'
+ ),
+ 'nexus_purchases' => array(
+ 'prefix' => 'ps_',
+ 'column' => 'conv_parent'
+ )
+ )
+ )
+ );
+
+ foreach( $parents AS $app => $tables )
+ {
+ if ( !is_null( $application ) )
+ {
+ if ( $application != $app )
+ {
+ continue;
+ }
+ }
+
+ if ( static::appisEnabled( $app ) )
+ {
+ foreach( $tables['tables'] AS $table => $data )
+ {
+ $column = $data['prefix'] . $data['column'];
+ if ( \IPS\Db::i()->checkForColumn( $table, $column ) === FALSE )
+ {
+ \IPS\Db::i()->addColumn( $table, array(
+ 'name' => $column,
+ 'type' => 'BIGINT',
+ 'length' => 20,
+ 'default' => 0,
+ ) );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Check if we need to redirect requests coming in to index.php
+ *
+ * @note In an ideal world, we'd check the individual converter libraries, however that requires looping over lots of files or
+ * querying the database on every single page load, so instead we will sniff and see if we think anything needs to be done based
+ * on hardcoded potential patterns.
+ * @return void
+ */
+ public function convertLegacyParameters()
+ {
+ $_qs = '';
+ if ( isset( $_SERVER['QUERY_STRING'] ) )
+ {
+ $_qs = $_SERVER['QUERY_STRING'];
+ }
+ elseif ( isset( $_SERVER['PATH_INFO'] ) )
+ {
+ $_qs = $_SERVER['PATH_INFO'];
+ }
+ elseif ( isset( $_SERVER['REQUEST_URI'] ) )
+ {
+ $_qs = $_SERVER['REQUEST_URI'];
+ }
+
+ /* Expression Engine */
+ preg_match ( '#(viewforum|viewthread|viewreply|member)\/([0-9]+)#i', $_qs, $matches );
+ if( isset( $matches[1] ) AND $matches[1] )
+ {
+ static::checkRedirects();
+ }
+
+ /* Vanilla */
+ preg_match ( '#(discussion|profile)\/([0-9]+)\/#i', $_qs, $matches );
+
+ if( isset( $matches[1] ) AND $matches[1] )
+ {
+ static::checkRedirects();
+ }
+
+ /* Xenforo */
+ preg_match ( '#(forums|threads|members)\/(.*)\.([0-9]+)#i', $_qs, $matches );
+
+ if( isset( $matches[1] ) AND $matches[1] )
+ {
+ static::checkRedirects();
+ }
+
+ /* SMF */
+ if( \IPS\Request::i()->board OR \IPS\Request::i()->topic OR \IPS\Request::i()->action )
+ {
+ static::checkRedirects();
+ }
+ }
+
+ /**
+ * Check if we need to redirect
+ *
+ * @return void
+ */
+ public static function checkRedirects()
+ {
+ /* Try each of our converted applications. We will assume the most important conversions were done first */
+ foreach( \IPS\convert\App::apps() as $app )
+ {
+ try
+ {
+ $redirect = $app->getSource( TRUE, FALSE )->checkRedirects();
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ /* This converter app doesn't exist on disk, this is expected for sites upgraded from 3.x where there isn't a 4.x version of the converter app */
+ continue;
+ }
+
+ /* Pass the request off to the application to see if it can redirect */
+ if( $redirect !== NULL )
+ {
+ /* We got a valid redirect, so send the user there */
+ \IPS\Output::i()->redirect( $redirect, NULL, 301 );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/api/converters.php b/applications/convert/api/converters.php
new file mode 100644
index 00000000..e1a5d50d
--- /dev/null
+++ b/applications/convert/api/converters.php
@@ -0,0 +1,105 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage convert
+ * @since 29 Nov 2016
+ * @version SVN_VERSION_NUMBER
+ */
+
+namespace IPS\convert\api;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * @brief Converters API
+ */
+class _converters extends \IPS\Api\Controller
+{
+ /**
+ * GET /convert/converters
+ * Get list of converter applications and what can be converted from each application
+ *
+ * @apiparam string applications Comma-delimited list of applications to filter by
+ * @return array
+ */
+ public function GETindex()
+ {
+ $response = array();
+ $applications = \IPS\Request::i()->applications ? explode( ',', \IPS\Request::i()->applications ) : array();
+
+ /* Loop over software directory to figure out software we can convert */
+ foreach( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/convert/sources/Software/' ) as $directory )
+ {
+ /* Skip ../ and any non-directories */
+ if( $directory->isDot() OR !$directory->isDir() )
+ {
+ continue;
+ }
+
+ /* Are we filtering by supported applications? */
+ if( count( $applications ) AND !in_array( mb_convert_case( $directory->getFilename(), MB_CASE_TITLE ) ) )
+ {
+ continue;
+ }
+
+ /* Loop over all of the software in this directory */
+ foreach( new \DirectoryIterator( $directory->getPathname() ) as $softwares )
+ {
+ /* Skip ../ and any non-files */
+ if( $softwares->isDot() OR !$softwares->isFile() )
+ {
+ continue;
+ }
+
+ /* Initialize class info, etc. */
+ $namespace = mb_substr( $softwares->getFilename(), 0, mb_strrpos( $softwares->getFilename(), '.' ) );
+ $application = $directory->getFilename();
+ $classname = "\\IPS\\convert\\Software\\" . $application . "\\" . $namespace;
+
+ /* Can we convert this application? */
+ if( $classname::canConvert() === NULL )
+ {
+ continue;
+ }
+
+ /* Initialize this application's array if necessary */
+ if( !isset( $response[ mb_strtolower( $namespace ) ] ) )
+ {
+ $response[ mb_strtolower( $namespace ) ] = array();
+ }
+
+ /* Now figure out what we can convert in a human-readable format */
+ $rawConvert = array_keys( $classname::canConvert() );
+ $canConvert = array();
+
+ /* Get lang strings for what we can convert */
+ foreach( $rawConvert as $convertType )
+ {
+ $canConvert[] = \IPS\Lang::load( \IPS\Lang::defaultLanguage() )->addToStack(
+ preg_replace_callback( "/([A-Z])/", function( $match ){
+ return "_" . mb_strtolower( $match[1] );
+ }, $convertType )
+ );
+ }
+
+ /* Now store the data for this application that we need */
+ $response[ mb_strtolower( $namespace ) ][ mb_strtolower( $application ) ] = array(
+ 'name' => $classname::softwareName(),
+ 'key' => $classname::softwareKey(),
+ 'canConvert' => $canConvert
+ );
+ }
+ }
+
+ return new \IPS\Api\Response( 200, $response );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/data/acpmenu.json b/applications/convert/data/acpmenu.json
new file mode 100644
index 00000000..e2d7c078
--- /dev/null
+++ b/applications/convert/data/acpmenu.json
@@ -0,0 +1,16 @@
+{
+ "manage": {
+ "manage_start": {
+ "tab": "core",
+ "controller": "create",
+ "do": "&_new=1",
+ "restriction": "manage_create"
+ },
+ "manage": {
+ "tab": "core",
+ "controller": "manage",
+ "do": "",
+ "restriction": "manage_manage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/data/acprestrictions.json b/applications/convert/data/acprestrictions.json
new file mode 100644
index 00000000..2e3d2638
--- /dev/null
+++ b/applications/convert/data/acprestrictions.json
@@ -0,0 +1,14 @@
+{
+ "start": [],
+ "manage": {
+ "manage": {
+ "manage_manage": "manage_manage"
+ },
+ "convert": {
+ "convert_manage": "convert_manage"
+ },
+ "create": {
+ "create_manage": "create_manage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/data/themesettings.json b/applications/convert/data/acpsearch.json
similarity index 100%
rename from applications/chat/data/themesettings.json
rename to applications/convert/data/acpsearch.json
diff --git a/applications/chat/data/application.json b/applications/convert/data/application.json
similarity index 65%
rename from applications/chat/data/application.json
rename to applications/convert/data/application.json
index 4ff3a539..c75c0da5 100644
--- a/applications/chat/data/application.json
+++ b/applications/convert/data/application.json
@@ -1,7 +1,7 @@
{
- "application_title": "Chat",
+ "application_title": "Converter",
"app_author": "Invision Power Services, Inc.",
- "app_directory": "chat",
+ "app_directory": "convert",
"app_protected": 0,
"app_website": null,
"app_update_check": null
diff --git a/applications/convert/data/defaults/Library.txt b/applications/convert/data/defaults/Library.txt
new file mode 100644
index 00000000..c9a5fd50
--- /dev/null
+++ b/applications/convert/data/defaults/Library.txt
@@ -0,0 +1,62 @@
+ Class
+ * @author Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _<#CLASS#> extends \IPS\convert\Library
+{
+ /**
+ * @brief Application
+ */
+ public $app = '<#CLASS_LOWER#>';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ return array();
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @param string The method to truncate.
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ return array();
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+}
\ No newline at end of file
diff --git a/applications/convert/data/defaults/Software.txt b/applications/convert/data/defaults/Software.txt
new file mode 100644
index 00000000..52881b6d
--- /dev/null
+++ b/applications/convert/data/defaults/Software.txt
@@ -0,0 +1,151 @@
+ Class
+ * @author Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\<#APP#>;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _<#CLASS#> extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "<#CLASS#>";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "<#CLASS_LOWER#>";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array|NULL
+ * @note NULL indicates this converter cannot be used yet, useful to move redirect scripts into the 4.x framework
+ */
+ public static function canConvert()
+ {
+ return array(
+
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Can we convert settings?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Settings Map
+ *
+ * @return array
+ */
+ public function settingsMap()
+ {
+ return array();
+ }
+
+ /**
+ * Settings Map Listing
+ *
+ * @return array
+ */
+ public function settingsMapList()
+ {
+ return array();
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string|NULL
+ */
+ public static function getPreConversionInformation()
+ {
+ return NULL;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array();
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ return array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ return array();
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return $post;
+ }
+
+ // Insert convertX methods here
+}
\ No newline at end of file
diff --git a/applications/chat/data/emails.xml b/applications/convert/data/emails.xml
similarity index 100%
rename from applications/chat/data/emails.xml
rename to applications/convert/data/emails.xml
diff --git a/applications/convert/data/hooks.json b/applications/convert/data/hooks.json
new file mode 100644
index 00000000..e0942351
--- /dev/null
+++ b/applications/convert/data/hooks.json
@@ -0,0 +1,10 @@
+{
+ "Db": {
+ "type": "C",
+ "class": "\\IPS\\Db"
+ },
+ "FrontDispatcher": {
+ "type": "C",
+ "class": "\\IPS\\Dispatcher\\Front"
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/data/index.html b/applications/convert/data/index.html
similarity index 100%
rename from applications/chat/data/index.html
rename to applications/convert/data/index.html
diff --git a/applications/convert/data/javascript.xml b/applications/convert/data/javascript.xml
new file mode 100644
index 00000000..73e68431
--- /dev/null
+++ b/applications/convert/data/javascript.xml
@@ -0,0 +1,63 @@
+
+
+ /**
+ * IPS Community Suite 4
+ * (c) 2016 Invision Power Services - http://www.invisionpower.com
+ *
+ * ips.convert.menu.js -
+ *
+ * Author: Ryan Ashbrook
+ */
+;( function($, _, undefined){
+ "use strict";
+
+ ips.controller.register('convert.admin.convert.menu', {
+
+ initialize: function () {
+ this.on( 'click', "[data-action='reConvert']", this.areYouSure );
+ this.on( 'click', "[data-action='remove_converted_data']", this.removeConverted );
+ },
+
+ areYouSure: function(e) {
+ var self = this;
+
+ e.preventDefault();
+
+ ips.ui.alert.show( {
+ type: 'confirm',
+ icon: 'warning',
+ message: 'Are you sure you wish to reconvert this?',
+ subText: 'Reconverting can cause data inconsistency. If you reconvert this step, you must also reconvert each step below it.',
+ } );
+ },
+
+ removeConverted: function(e) {
+ var self = this;
+
+ e.preventDefault();
+
+ ips.ui.alert.show( {
+ type: 'confirm',
+ icon: 'warning',
+ message: 'Are you sure you wish to remove all converted data for this step?',
+ subText: 'Removing all converted data will remove all data that has been converted for this step. This action cannot be undone.',
+ } );
+ }
+ } );
+}(jQuery, _));
+ templates
+common/ips.loader.js
+common/ui
+common/utils
+common
+controllers
+ underscore
+jquery
+mustache
+jstz
+Debug.js
+app.js
+ jquery.js
+jquery.history.js
+jquery.transform.js
+
diff --git a/applications/convert/data/lang.xml b/applications/convert/data/lang.xml
new file mode 100644
index 00000000..0a2d5744
--- /dev/null
+++ b/applications/convert/data/lang.xml
@@ -0,0 +1,288 @@
+
+
+
+ Converter
+ Conversions
+ Start New Conversion
+ Manage Conversions
+ Suite Application
+ Source Software
+ Identifier
+ Parent Conversion
+ No Parent]]>
+ Start New Conversion
+ Conversion Deleted
+ This conversion has `child` conversions assigned to it, these must be deleted first
+ A conversion has already been configured for this software. We have taken you to that conversion now.
+ The conversion you were managing could not be found
+ What do you want to convert?
+ Member and System Information
+ Forum and Topic Data
+ Blog Data
+ Calendar and Event Data
+ Downloads Data
+ Gallery and Images Data
+ Pages and Record Data
+ Commerce, Support, and Purchase Data
+ What software are you converting from?
+ This converter requires that a parent conversion exists first.
+ Database Details
+ If this is from the same source as a previous conversion, select it here.
+ Parent Conversion
+ If this conversion requires different database details then the parent, then enter them here - otherwise leave blank to use the parent details.
+ Database Host
+ Database Port
+ Database Name
+ Database User
+ Database Password
+ Database Table Prefix
+ Database Character Set
+ Select Source Software
+ Enter Database Details
+ Start Conversion
+ Edit Conversion
+ Database Host
+ Database Port
+ Database Name
+ Database User
+ Database Password
+ Database Table Prefix
+ Database Character Set
+ Failed to count rows in table '%s' - is your table prefix set correctly?
+ Location of files
+ Create New Converter
+ Create New Library
+ Class Name
+ Library
+ You can only create new converters while IN_DEV.
+ You can only create new libraries while IN_DEV.
+ Path to %s installation.
+ The path entered appears to be invalid.
+ Finishing the conversion should only be done once all parts of your conversion is completed
+ Finish Conversion
+ The details provided are not correct or the database is unavailable.
+ The database character set you supplied is not a valid database character set.
+ The database table prefix you supplied appears to be incorrect.
+ Convert Again
+ Begin Conversion
+ Are you sure you wish to convert again? If you reconvert this step, then you MUST also reconvert the following steps.
+ Reconvert
+ begin another conversion or finish to rebuild data.]]>
+ Note: Items that contain customized HTML, such as pages and blocks, may not work properly when converted into the IPS Community Suite. It is recommended that you review these items, once the conversion has completed, and update them accordingly.]]>
+ This is typically: /path/to/wordpress/wp-content/uploads
+ This is typically: /path/to/expressionengine/images/signature_attachments/
+ This is typically: /path/to/expressionengine/images/avatars/
+ This is typically: /path/to/expressionengine/images/pm_attachments/
+ This is typically: /path/to/expressionengine/images/forum_attachments
+ (Optional) Location where group images or icons are stored
+ This is typically: /path/to/phpbb/images/smilies
+ This is the string of characters that precedes the filename of all uploaded avatars - if you are not sure what this is, you can typically find it by referencing the files located at /path/to/phpbb/images/avatars/upload
+ This is typically: /path/to/phpbb/images/avatars/upload
+ This is typically: /path/to/phpbb/images/avatars/gallery - leave blank to skip converting avatars from the avatar gallery
+ This is typically: /path/to/phpbb/images/ranks
+ This is typically: /path/to/phpbb/files
+ This is typically: /path/to/vanilla/uploads
+ This is typically: /path/to/vb/images/smilies
+ This is typically: /path/to/vb5/core/images/smilies
+ This is typically: /path/to/xenforo/internal_data/attachments
+ This is typically: /path/to/xenforo/data/xengallery
+ This is typically: /path/to/xenforo/data/avatars
+ This is typically: /path/to/mybb/uploads
+ This is typically: /path/to/smf/attachments
+ This is typically: /path/to/ubb/uploads/attachments
+ If you are also converting from vBulletin Forums, it is highly recommended that you convert from that application first so article to topic synchronization can occur correctly.
+ You can typically obtain these details from the file located at /path/to/phpbb/config.php
+ You can typically obtain these details from the file located at /path/to/vanilla/conf/config.php
+ You can typically obtain these details from the file located at /path/to/vb4/includes/config.php
+ You can typically obtain these details from the file located at /path/to/vb5/core/includes/config.php
+ You can typically obtain these details from the file called wp-config.php
+ You can typically obtain these details from the file located at /path/to/xenforo/library/config.php
+ This is typically: /path/to/coppermine/albums
+ This is typically: /path/to/photoplog/images
+ Rewrite Urls in the Community Suite. If you do not enable this option, URLs from your previous software installation may not correctly redirect to the new locations in the Community Suite.]]>
+ click here before beginning.]]>
+ Converting Settings
+ The following settings will converted to their IPS Community Suite equivalents.
+ %s to %s : %s]]>
+ Converting from %s to %s
+ Step
+ Current Count
+ Source Count
+ Amount to Process Per Cycle
+ Empty Local Data
+ Remove Converted Data
+
+ More Information Needed
+ Converting...
+ Converted %s %s out of %s
+ Removed %s %s out of %s
+ Finish
+ Reconfigure
+ Cannot Convert Yet
+ This step cannot be run until the steps before this one have been run.
+ The conversion you attempted to run could not be found.
+ Custom BBCode
+ Emoticons
+ Path to emoticons
+ Keep Existing?
+ If checked, then any emoticons that are inserted that have the same typed code as an existing emoticon will be merged into the already existing one.
+ Emoticon missing ID
+ Emoticon missing code
+ No emoticon file provided
+ No emoticon filename provided
+ Follow failed to insert
+ Profile Field Groups
+ Profile Field Group missing ID
+ Profile Field Group missing name
+ Select the profile field group to merge this one with, or select 'None' to create new.
+ Profile Fields
+ Select the profile field to merge this one with, or select 'None' to create new.
+ Member Groups
+ Select the group to merge this one with, or select 'None' to create new.
+ Members
+ Member followers
+ Photos to Retain
+ This software supports multiple types of images to represent an account. Only one of these can be retained, however. If a photo of the selected type does not exist for a member, than the other will attempt to be used instead.
+ Location of Photos
+ If member photos are presently stored in the database, then select 'Database', otherwise select 'File System' and then enter the path to profile photos in the textbox provided.
+ The path to profile photos.
+ Avatar Hash
+ Gallery Location
+ Field: %s
+ This field is stored directly in the database, rather than as a custom profile field. You can choose to either convert it into a new profile field, or not convert it at all. If creating a new field, then you may enter the name of the field into the textbox.
+ Avatars
+ Profile Photos
+ Database
+ File System
+ Do not convert
+ Create New Field
+ Member %s exists as %s.
+ Member email invalid (%s) using %s instead.
+ Name '%s' is currently in use, member renamed to '%s'
+ Name '%s' is too long, member renamed to '%s'
+ Name '%s' contained invalid characters, member renamed to '%s'
+ Name to keep
+ User Name
+ Display Name
+ You can only convert one name, do you want to keep usernames or display names?
+ Status Updates / Profile Comments
+ Status Comments
+ Status Update Missing ID
+ No Member ID provided.
+ No Author ID provided
+ No comment text
+ Ignored Users
+ Missing Ignore ID
+ No Member ID provided
+ No Ignored Member ID provided
+ Announcements
+ Announcement Missing ID
+ Announcement Missing Title
+ Announcement Missing Content
+ Announcement Missing Member ID
+ Private Messages
+ PM missing topic ID
+ PM does not have any posts
+ PM does not have any users
+ PM title is missing.
+ PM starter missing.
+ PM post missing ID
+ PM post missing content
+ PM map missing user
+ No member maps inserted.
+ Private Message Replies
+ Display Name Change Logs
+ Member Ranks
+ Rank ID Missing
+ Rank Missing Title
+ Rank Image Location
+ Duplicate Rating
+ Profanity Filters
+ Ban Filters
+ Ban Filters
+ Question and Answer Verification
+ Warning Actions
+ Warning Reasons
+ Forums
+ Forum followers
+ Password for %s
+ Note: Not entering a password here will result in no password being set for the forum.]]>
+ Topics
+ Topic followers
+ Posts
+ Archived Posts
+ Attachments
+ Post Attachments
+ Path to attachments
+ Blogs
+ Blog Entries
+ Entry Comments
+ Entry Attachments
+ Calendars
+ Calendar Events
+ Event Comments
+ Event Reviews
+ Event Attachments
+ Categories
+ Albums
+ Images
+ Image Comments
+ Image Reviews
+ Image Attachments
+ Members Gallery Category
+ IPS Community Suite 4 requires all albums belong to a category. Please select the category to place albums with no category into, or select None to create one automatically.
+ Media Thumbnail Location
+ Convert author replies to comments?
+ If Yes, then replies by the author to reviews will be converted into normal comments, with the review in quotes. If No, replies will not be converted.
+ File Custom Fields
+ File Categories
+ Files
+ File Reviews
+ File Comments
+ Pages
+ Databases
+ Database Categories
+ Database Records
+ Database Comments
+ Media
+ Blocks
+ Deleting empty topics
+ Rebuilding Gallery Images
+ Rebuilding Personal Conversation first post ids
+ Rebuilding %s tags
+ Rebuilding Question Ratings
+ Rebuilding Answer Ratings
+ Continue Conversion
+ Customers
+ Package Groups
+ Packages
+ Invoices
+ Transactions
+ Purchases
+ Rebuilding blog entries
+ Rebuilding entry comments
+ Resyncing blog counts
+ Rebuilding blog entry tags
+ Rebuilding gallery images
+ Resyncing gallery category counts
+ Resyncing gallery album counts
+ Resyncing gallery image counts
+ Rebuilding gallery image tags
+ Resyncing forum last post information
+ Rebuilding posts
+ Resyncing forum counts
+ Resyncing topic counts
+ Rebuilding topic tags
+ Rebuilding search index
+ Clearing caches
+ Rebuilding private messages
+ Rebuilding signatures
+ Rebuilding announcements
+ Rebuilding attachment thumbnails
+ Resyncing database category counts
+ Rebuilding database tags
+ Rebuilding calendar events
+ Resyncing calendar and event counts
+ Converters
+
+
diff --git a/applications/convert/data/modules.json b/applications/convert/data/modules.json
new file mode 100644
index 00000000..6518aa9b
--- /dev/null
+++ b/applications/convert/data/modules.json
@@ -0,0 +1,8 @@
+{
+ "admin": {
+ "manage": {
+ "default_controller": "manage",
+ "protected": 0
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/data/schema.json b/applications/convert/data/schema.json
new file mode 100644
index 00000000..f4e60ac3
--- /dev/null
+++ b/applications/convert/data/schema.json
@@ -0,0 +1,1414 @@
+{
+ "convert_apps": {
+ "name": "convert_apps",
+ "columns": {
+ "app_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "app_id",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "sw": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 32,
+ "name": "sw",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app_key": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 32,
+ "name": "app_key",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "name": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "name",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "login": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "login",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "parent": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "parent",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_driver": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 32,
+ "name": "db_driver",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_host": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 128,
+ "name": "db_host",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_user": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 128,
+ "name": "db_user",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_pass": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 128,
+ "name": "db_pass",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_db": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 128,
+ "name": "db_db",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_prefix": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 32,
+ "name": "db_prefix",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "db_charset": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 32,
+ "name": "db_charset",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app_merge": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "1",
+ "length": 1,
+ "name": "app_merge",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "start_date": {
+ "name": "start_date",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "0",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "db_port": {
+ "name": "db_port",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "3306",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "app_id"
+ ]
+ },
+ "name": {
+ "type": "key",
+ "name": "name",
+ "length": [
+ 191
+ ],
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "collation": "utf8mb4_unicode_ci",
+ "engine": "InnoDB"
+ },
+ "convert_link": {
+ "name": "convert_link",
+ "columns": {
+ "link_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "link_id",
+ "type": "INT",
+ "unsigned": true,
+ "values": [],
+ "zerofill": false
+ },
+ "ipb_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "ipb_id",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "foreign_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 255,
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "type": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 32,
+ "name": "type",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "duplicate": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "duplicate",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "app",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "link_id"
+ ]
+ },
+ "foreign_id": {
+ "type": "key",
+ "name": "foreign_id",
+ "length": [
+ 191
+ ],
+ "columns": [
+ "foreign_id"
+ ]
+ },
+ "local_lookup": {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ },
+ "type": {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "app": {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ },
+ "collation": "utf8mb4_unicode_ci",
+ "engine": "InnoDB"
+ },
+ "convert_link_pms": {
+ "name": "convert_link_pms",
+ "columns": {
+ "link_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "link_id",
+ "type": "INT",
+ "unsigned": true,
+ "values": [],
+ "zerofill": false
+ },
+ "ipb_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "ipb_id",
+ "type": "BIGINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "foreign_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "type": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 32,
+ "name": "type",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "duplicate": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "duplicate",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "app",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "link_id"
+ ]
+ },
+ "foreign_id": {
+ "type": "key",
+ "name": "foreign_id",
+ "length": [
+ 191
+ ],
+ "columns": [
+ "foreign_id"
+ ]
+ },
+ "local_lookup": {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ },
+ "type": {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "app": {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ },
+ "collation": "utf8mb4_unicode_ci",
+ "engine": "InnoDB"
+ },
+ "convert_link_posts": {
+ "name": "convert_link_posts",
+ "columns": {
+ "link_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "link_id",
+ "type": "INT",
+ "unsigned": true,
+ "values": [],
+ "zerofill": false
+ },
+ "ipb_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "ipb_id",
+ "type": "BIGINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "foreign_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "type": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 32,
+ "name": "type",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "duplicate": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "duplicate",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "app",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "link_id"
+ ]
+ },
+ "foreign_id": {
+ "type": "key",
+ "name": "foreign_id",
+ "length": [
+ 191
+ ],
+ "columns": [
+ "foreign_id"
+ ]
+ },
+ "local_lookup": {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ },
+ "type": {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "app": {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ },
+ "collation": "utf8mb4_unicode_ci",
+ "engine": "InnoDB"
+ },
+ "convert_link_topics": {
+ "name": "convert_link_topics",
+ "columns": {
+ "link_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "link_id",
+ "type": "INT",
+ "unsigned": true,
+ "values": [],
+ "zerofill": false
+ },
+ "ipb_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "ipb_id",
+ "type": "BIGINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "foreign_id": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "type": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 32,
+ "name": "type",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "duplicate": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "duplicate",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "app": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 10,
+ "name": "app",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "link_id"
+ ]
+ },
+ "foreign_id": {
+ "type": "key",
+ "name": "foreign_id",
+ "length": [
+ 191
+ ],
+ "columns": [
+ "foreign_id"
+ ]
+ },
+ "local_lookup": {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ },
+ "type": {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "app": {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ },
+ "collation": "utf8mb4_unicode_ci",
+ "engine": "InnoDB"
+ },
+ "convert_logs": {
+ "name": "convert_logs",
+ "columns": {
+ "log_id": {
+ "name": "log_id",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": null,
+ "comment": "ID Number",
+ "unsigned": true,
+ "zerofill": false,
+ "auto_increment": true,
+ "binary": false
+ },
+ "log_message": {
+ "name": "log_message",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Log Message",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_app": {
+ "name": "log_app",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "The conversion app.",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_method": {
+ "name": "log_method",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Conversion step that triggered this log",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_severity": {
+ "name": "log_severity",
+ "type": "TINYINT",
+ "length": 1,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "Flag to indicate the type of log (notice, warning, error)",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_item_id": {
+ "name": "log_item_id",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": null,
+ "allow_null": true,
+ "default": null,
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_time": {
+ "name": "log_time",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "0",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "columns": [
+ "log_id"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "log_app": {
+ "type": "key",
+ "name": "log_app",
+ "columns": [
+ "log_app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ }
+ },
+ "convert_app_sessions": {
+ "name": "convert_app_sessions",
+ "columns": {
+ "session_app_id": {
+ "name": "session_app_id",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "ID Number",
+ "unsigned": true,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "session_app_data": {
+ "name": "session_app_data",
+ "type": "MEDIUMTEXT",
+ "length": null,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Session Data containing rows converted, completed steps, etc.",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "columns": [
+ "session_app_id"
+ ],
+ "length": [
+ null
+ ]
+ }
+ },
+ "comment": ""
+ },
+ "convert_custom_bbcode": {
+ "name": "convert_custom_bbcode",
+ "columns": {
+ "bbcode_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "bbcode_id",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_title": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "bbcode_title",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_desc": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "bbcode_desc",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_tag": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "bbcode_tag",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_replace": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "bbcode_replace",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_useoption": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_useoption",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_example": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "bbcode_example",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_switch_option": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_switch_option",
+ "type": "INT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_menu_option_text": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 200,
+ "name": "bbcode_menu_option_text",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_menu_content_text": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 200,
+ "name": "bbcode_menu_content_text",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_single_tag": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_single_tag",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_groups": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 255,
+ "name": "bbcode_groups",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_sections": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 255,
+ "name": "bbcode_sections",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_php_plugin": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 255,
+ "name": "bbcode_php_plugin",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_no_parsing": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_no_parsing",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_protected": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_protected",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_aliases": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 255,
+ "name": "bbcode_aliases",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_optional_option": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 1,
+ "name": "bbcode_optional_option",
+ "type": "TINYINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_image": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 255,
+ "name": "bbcode_image",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_app": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 50,
+ "name": "bbcode_app",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "bbcode_custom_regex": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "bbcode_custom_regex",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "bbcode_id"
+ ]
+ }
+ },
+ "comment": ""
+ },
+ "convert_bbcode_mediatag": {
+ "name": "convert_bbcode_mediatag",
+ "columns": {
+ "mediatag_id": {
+ "allow_null": false,
+ "auto_increment": true,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 10,
+ "name": "mediatag_id",
+ "type": "SMALLINT",
+ "unsigned": true,
+ "values": [],
+ "zerofill": false
+ },
+ "mediatag_name": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "",
+ "length": 255,
+ "name": "mediatag_name",
+ "type": "VARCHAR",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "mediatag_match": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "mediatag_match",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "mediatag_replace": {
+ "allow_null": true,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": null,
+ "length": 0,
+ "name": "mediatag_replace",
+ "type": "TEXT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ },
+ "mediatag_position": {
+ "allow_null": false,
+ "auto_increment": false,
+ "binary": false,
+ "comment": "",
+ "decimals": null,
+ "default": "0",
+ "length": 6,
+ "name": "mediatag_position",
+ "type": "SMALLINT",
+ "unsigned": false,
+ "values": [],
+ "zerofill": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "length": [
+ null
+ ],
+ "columns": [
+ "mediatag_id"
+ ]
+ }
+ },
+ "comment": ""
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/setup/upg_40000/queries.json b/applications/convert/data/settings.json
similarity index 100%
rename from applications/chat/setup/upg_40000/queries.json
rename to applications/convert/data/settings.json
diff --git a/applications/convert/data/tasks.json b/applications/convert/data/tasks.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/applications/convert/data/tasks.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/applications/convert/data/theme.xml b/applications/convert/data/theme.xml
new file mode 100644
index 00000000..8681f68f
--- /dev/null
+++ b/applications/convert/data/theme.xml
@@ -0,0 +1,74 @@
+
+
+ {lang="converting_settings_desc"}
+
+ {{foreach $settingsToConvert AS $setting}}
+ {$setting['title']} {lang="to"} {$setting['our_title']} : {$setting['value']}
+ {{endforeach}}
+ ]]>
+ extra['softwareClass'];}}
+{{foreach $rows as $r}}
+
+{{endforeach}}]]>
+
+ {$info|raw}
+]]>
+ app_id}" );}}
+
+ {lang="converter_supports_settings" sprintf="$url"}
+
]]>
+
diff --git a/applications/convert/data/themesettings.json b/applications/convert/data/themesettings.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/applications/convert/data/themesettings.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/applications/convert/data/versions.json b/applications/convert/data/versions.json
new file mode 100644
index 00000000..f74ea2d5
--- /dev/null
+++ b/applications/convert/data/versions.json
@@ -0,0 +1,56 @@
+{
+ "32001": "1.2.2",
+ "33001": "1.2.3",
+ "34000": "1.2.4",
+ "34100": "1.2.5",
+ "34200": "1.2.6",
+ "34300": "1.2.7",
+ "34400": "1.2.8",
+ "34500": "1.2.9",
+ "34600": "1.2.10",
+ "34700": "1.2.11",
+ "34800": "1.2.12",
+ "34900": "1.2.13",
+ "34901": "1.2.14",
+ "34902": "1.2.15",
+ "100000": "4.0.0",
+ "101000": "4.1.0 Beta 1",
+ "101001": "4.1.0 Beta 2",
+ "101002": "4.1.0 Beta 3",
+ "101003": "4.1.0",
+ "101004": "4.1.1",
+ "101005": "4.1.2",
+ "101006": "4.1.3",
+ "101007": "4.1.4",
+ "101008": "4.1.5",
+ "101009": "4.1.6",
+ "101010": "4.1.7",
+ "101011": "4.1.8",
+ "101012": "4.1.9",
+ "101070": "4.1.16.1",
+ "101071": "4.1.17 Beta 1",
+ "101072": "4.1.17 Beta 1a",
+ "101073": "4.1.17 Beta 1b",
+ "101074": "4.1.17 Beta 2",
+ "101075": "4.1.17 Beta 3",
+ "101076": "4.1.17 Beta 3",
+ "101077": "4.1.17",
+ "101078": "4.1.17.1",
+ "101079": "4.1.18 Beta 1",
+ "101082": "4.1.18 Beta 1b",
+ "101083": "4.1.18 Beta 2",
+ "101084": "4.1.18 Beta 3",
+ "101085": "4.1.18",
+ "101088": "4.1.18.1",
+ "101089": "4.1.18.2",
+ "101090": "4.1.19 Beta 1",
+ "101091": "4.1.19 Beta 2",
+ "101092": "4.1.19 Beta 3",
+ "101093": "4.1.19",
+ "101094": "4.1.19.1",
+ "101095": "4.1.19.2 Beta 1",
+ "101096": "4.1.19.2",
+ "101097": "4.1.19.2",
+ "101098": "4.1.19.3",
+ "101099": "4.1.19.4"
+}
\ No newline at end of file
diff --git a/applications/convert/data/widgets.json b/applications/convert/data/widgets.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/applications/convert/data/widgets.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/DeleteEmptyTopics.php b/applications/convert/extensions/core/Queue/DeleteEmptyTopics.php
new file mode 100644
index 00000000..440cfe4f
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/DeleteEmptyTopics.php
@@ -0,0 +1,143 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 26 July 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _DeleteEmptyTopics
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ try
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(tid)', 'forums_topics' )->first();
+ }
+ catch( \Exception $e )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int|null New offset or NULL if complete
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ if ( !class_exists( 'IPS\forums\Topic' ) OR !\IPS\Application::appisEnabled( 'forums' ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* If app was removed, then cancel this */
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ $last = NULL;
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'forums_topics', array( "tid>?", $offset ), "tid ASC", array( 0, 50 ) ), 'IPS\forums\Topic' ) AS $topic )
+ {
+ $tid = $topic->tid;
+
+ /* Is this converted content? */
+ try
+ {
+ /* Just checking, we don't actually need anything */
+ $app->checkLink( $tid, 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $last = $tid;
+ continue;
+ }
+
+ if ( $topic->isArchived() == FALSE )
+ {
+ try
+ {
+ \IPS\Db::i()->select( 'pid', 'forums_posts', array( 'topic_id=?', $topic->tid ), NULL, 1 )->first();
+ }
+ /* This topic is empty */
+ catch( \UnderflowException $e )
+ {
+ $topic->delete();
+ }
+ }
+ else
+ {
+ try
+ {
+ /* Set first post */
+ \IPS\forums\Topic\ArchivedPost::db()->select( 'archive_id', 'forums_archive_posts', array( "archive_topic_id=?", $topic->tid ), NULL, 1 )->first();
+ }
+ /* This topic is empty */
+ catch( \UnderflowException $e )
+ {
+ $topic->delete();
+ }
+ }
+
+ $last = $tid;
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaning task and percentage complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'queue_deleting_empty_topics' ), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildContent.php b/applications/convert/extensions/core/Queue/RebuildContent.php
new file mode 100644
index 00000000..50641e42
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildContent.php
@@ -0,0 +1,278 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 10 Sep 2015
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildContent
+{
+ /**
+ * @brief Number of content items to rebuild per cycle
+ */
+ public $rebuild = 50;
+
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ $classname = $data['class'];
+
+ \IPS\Log::debug( "Getting preQueueData for " . $classname, 'rebuildPosts' );
+
+ try
+ {
+ $data['count'] = $classname::db()->select( 'MAX(' . $classname::$databasePrefix . $classname::$databaseColumnId . ')', $classname::$databaseTable, ( is_subclass_of( $classname, 'IPS\Content\Comment' ) ) ? $classname::commentWhere() : array() )->first();
+ $data['realCount'] = $classname::db()->select( 'COUNT(*)', $classname::$databaseTable, ( is_subclass_of( $classname, 'IPS\Content\Comment' ) ) ? $classname::commentWhere() : array() )->first();
+
+ /* We're going to use the < operator, so we need to ensure the most recent item is rebuilt */
+ $data['runPid'] = $data['count'] + 1;
+ }
+ catch( \Exception $ex )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ \IPS\Log::debug( "PreQueue count for " . $classname . " is " . $data['count'], 'rebuildPosts' );
+
+ if( $data['count'] == 0 )
+ {
+ return null;
+ }
+
+ $data['indexed'] = 0;
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int|null New offset or NULL if complete
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( &$data, $offset )
+ {
+ $classname = $data['class'];
+ $exploded = explode( '\\', $classname );
+ if ( !class_exists( $classname ) or !\IPS\Application::appIsEnabled( $exploded[1] ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Make sure there's even content to parse */
+ if( !isset( $classname::$databaseColumnMap['content'] ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Intentionally no try/catch as it means app doesn't exist */
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ $softwareClass = $app->getSource( FALSE, FALSE );
+
+ \IPS\Log::debug( "Running " . $classname . ", with an offset of " . $offset, 'rebuildPosts' );
+
+ $where = ( is_subclass_of( $classname, 'IPS\Content\Comment' ) ) ? ( is_array( $classname::commentWhere() ) ? array( $classname::commentWhere() ) : array() ) : array();
+ $select = $classname::db()->select( '*', $classname::$databaseTable, array_merge( $where, array( array( $classname::$databasePrefix . $classname::$databaseColumnId . ' < ?', $data['runPid'] ) ) ), $classname::$databasePrefix . $classname::$databaseColumnId . ' DESC', array( 0, $this->rebuild ) );
+ $iterator = new \IPS\Patterns\ActiveRecordIterator( $select, $classname );
+ $last = NULL;
+
+ foreach( $iterator as $item )
+ {
+ $idColumn = $classname::$databaseColumnId;
+
+ /* Is this converted content? */
+ try
+ {
+ /* Just checking, we don't actually need anything */
+ $app->checkLink( $item->$idColumn, $data['link'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $last = $item->$idColumn;
+ continue;
+ }
+
+ /* Did the rebuild previously time out on this? If so we need to skip it and move along */
+ if( isset( \IPS\Data\Store::i()->currentRebuild ) )
+ {
+ /* If the last rebuild cycle timed out, currentRebuild might be set and we might have already rebuilt this post (the post that caused the rebuild to fail might come after this (but before in chronological order).
+ If that is the case, we should skip rebuilding this post again. */
+ if( is_array( \IPS\Data\Store::i()->currentRebuild ) AND \IPS\Data\Store::i()->currentRebuild[0] == $classname AND \IPS\Data\Store::i()->currentRebuild[1] < $item->$idColumn )
+ {
+ $last = $item->$idColumn;
+ continue;
+ }
+
+ /* If the last rebuild cycle failed and we have just retrieved the post we last attempted to rebuild, skip it and move along */
+ if( is_array( \IPS\Data\Store::i()->currentRebuild ) AND \IPS\Data\Store::i()->currentRebuild[0] == $classname AND \IPS\Data\Store::i()->currentRebuild[1] == $item->$idColumn )
+ {
+ unset( \IPS\Data\Store::i()->currentRebuild );
+ $last = $item->$idColumn;
+ continue;
+ }
+ }
+
+ $member = \IPS\Member::load( $item->mapped('author') );
+ $extensions = \IPS\Application::load( $classname::$application )->extensions( 'core', 'EditorLocations' );
+
+ if( isset( $classname::$itemClass ) )
+ {
+ $itemClass = $classname::$itemClass;
+ $module = ucfirst( $itemClass::$module );
+ }
+ else
+ {
+ $module = ucfirst( $classname::$module );
+ }
+
+ $extension = NULL;
+
+ if ( isset( $extensions[ $module ] ) )
+ {
+ $extension = $extensions[ $module ];
+ }
+
+ $canUseHtml = (bool) $member->group['g_dohtml'];
+
+ if ( $extension )
+ {
+ $extensionCanUseHtml = $extension->canUseHtml( $member );
+ if ( $extensionCanUseHtml !== NULL )
+ {
+ $canUseHtml = $extensionCanUseHtml;
+ }
+ }
+
+ if ( ! $canUseHtml )
+ {
+ $canUseHtml = $item->htmlParsingEnforced();
+ }
+
+ $contentColumn = $classname::$databaseColumnMap['content'];
+
+ /* Figure out ids for attachments */
+ if( isset( $classname::$itemClass ) )
+ {
+ $itemIdColumn = $itemClass::$databaseColumnId;
+ $module = $itemClass::$module;
+
+ try
+ {
+ $id1 = $item->item()->$itemIdColumn;
+ }
+ catch ( \OutOfRangeException $e )
+ {
+ /* Post is orphaned and can be skipped */
+ $last = $item->$idColumn;
+ continue;
+ }
+
+ $id2 = $item->$idColumn;
+ }
+ else
+ {
+ $id1 = $item->$idColumn;
+ $id2 = 0;
+ $module = $classname::$module;
+ }
+
+ /* Before we start trying to rebuild, set a flag to note what we are trying to rebuild. If it times out, we can check
+ this on the next load and skip the problematic content */
+ \IPS\Data\Store::i()->currentRebuild = array( $classname, $item->$idColumn );
+
+ try
+ {
+ /* Pass off to software class */
+ if ( $softwareClass::$contentFixed === FALSE )
+ {
+ $item->$contentColumn = $softwareClass::fixPostData( $item->$contentColumn );
+ }
+
+ /* Then pass off to legacy parser to bring up to IPS4 */
+ $item->$contentColumn = \IPS\Text\LegacyParser::parseStatic( $item->$contentColumn, $member, $canUseHtml, $classname::$application . '_' . ucfirst( $module ), $id1, $id2, NULL, isset( $classname::$itemClass ) ? $classname::$itemClass : get_class( $item ) );
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ if( $e->getcode() == 103014 )
+ {
+ $item->$contentColumn = preg_replace( "#\[/?([^\]]+?)\]#", '', $item->$contentColumn );
+ }
+ else
+ {
+ throw $e;
+ }
+ }
+
+ $item->save();
+
+ $last = $item->$idColumn;
+
+ $data['indexed']++;
+
+ /* Now we will reset the rebuild flag we previously set since it rebuilt and saved successfully */
+ unset( \IPS\Data\Store::i()->currentRebuild );
+ }
+
+ /* Store the runPid for the next iteration of this Queue task. This allows the progress bar to show correctly. */
+ $data['runPid'] = $last;
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Return the number rebuilt so far, so that the rebuild progress bar text makes sense */
+ return $data['indexed'];
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ $class = $data['class'];
+ $exploded = explode( '\\', $class );
+ if ( !class_exists( $class ) or !\IPS\Application::appIsEnabled( $exploded[1] ) )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack('rebuilding_stuff', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl', FALSE, array( 'strtolower' => TRUE ) ) ) ) ), 'complete' => $data['realCount'] ? ( round( 100 / $data['realCount'] * $data['indexed'], 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildConversationFirstIds.php b/applications/convert/extensions/core/Queue/RebuildConversationFirstIds.php
new file mode 100644
index 00000000..12eff6b2
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildConversationFirstIds.php
@@ -0,0 +1,115 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 28 July 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildConversationFirstIds
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ try
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(mt_id)', 'core_message_topics' )->first();
+ }
+ catch( \Exception $e )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int New offset
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ $last = NULL;
+
+ $topicIdsToReset = array();
+ $firstPostIds = array();
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_message_topics', array( "mt_id>?", $offset ), "mt_id ASC", array( 0, 50 ) ), 'IPS\core\Messenger\Conversation' ) AS $conversation )
+ {
+ try
+ {
+ /* Set first post */
+ $conversation->first_msg_id = \IPS\Db::i()->select( 'msg_id', 'core_message_posts', array( 'msg_topic_id=?', $conversation->id ), 'msg_date ASC', 1 )->first();
+ $conversation->save();
+
+ /* Reset new_topic value for topic */
+ $topicIdsToReset[] = $conversation->id;
+ $firstPostIds[] = $conversation->first_msg_id;
+ }
+ /* Underflow exception may occur if the topic doesn't have any posts for an unknown reason */
+ catch( \UnderflowException $e ) {}
+
+ $last = $conversation->id;
+ }
+
+ /* Reset flags as needed */
+ if( count( $topicIdsToReset ) )
+ {
+ \IPS\Db::i()->update( 'core_message_posts', array( 'msg_is_first_post' => 0 ), array( 'msg_topic_id IN(' . implode( ',', $topicIdsToReset ) . ')' ) );
+ }
+
+ if( count( $firstPostIds ) )
+ {
+ \IPS\Db::i()->update( 'core_message_posts', array( 'msg_is_first_post' => 1 ), array( 'msg_id IN(' . implode( ',', $firstPostIds ) . ')' ) );
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack('queue_rebuilding_conversation_first_id'), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildFirstPostIds.php b/applications/convert/extensions/core/Queue/RebuildFirstPostIds.php
new file mode 100644
index 00000000..2efa72cb
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildFirstPostIds.php
@@ -0,0 +1,150 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 26 Feb 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildFirstPostIds
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ try
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(tid)', 'forums_topics' )->first();
+ }
+ catch( \Exception $e )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int New offset
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ if ( !class_exists( 'IPS\forums\Topic' ) OR !\IPS\Application::appisEnabled( 'forums' ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ $last = NULL;
+
+ $topicIdsToReset = array();
+ $archivedTopicIdsToReset = array();
+ $firstPostIds = array();
+ $firstPostArchivedIds = array();
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'forums_topics', array( "tid>?", $offset ), "tid ASC", array( 0, 50 ) ), 'IPS\forums\Topic' ) AS $topic )
+ {
+ if ( $topic->isArchived() == FALSE )
+ {
+ try
+ {
+ /* Set first post */
+ $topic->topic_firstpost = \IPS\Db::i()->select( 'pid', 'forums_posts', array( 'topic_id=?', $topic->tid ), 'post_date ASC', 1 )->first();
+ $topic->save();
+
+ /* Reset new_topic value for topic */
+ $topicIdsToReset[] = $topic->tid;
+ $firstPostIds[] = $topic->topic_firstpost;
+ }
+ /* Underflow exception may occur if the topic doesn't have any posts for an unknown reason */
+ catch( \UnderflowException $e ) {}
+ }
+ else
+ {
+ try
+ {
+ /* Set first post */
+ $topic->topic_firstpost = \IPS\forums\Topic\ArchivedPost::db()->select( 'archive_id', 'forums_archive_posts', array( "archive_topic_id=?", $topic->tid ), "archive_content_date ASC", 1 )->first();
+ $topic->save();
+
+ /* Reset new_topic value for topic */
+ $archivedTopicIdsToReset[] = $topic->tid;
+ $firstPostArchivedIds[] = $topic->topic_firstpost;
+ }
+ /* Underflow exception may occur if the topic doesn't have any posts for an unknown reason */
+ catch( \UnderflowException $e ) {}
+ }
+
+ $last = $topic->tid;
+ }
+
+ /* Reset flags as needed */
+ if( count( $topicIdsToReset ) )
+ {
+ \IPS\Db::i()->update( 'forums_posts', array( 'new_topic' => 0 ), array( 'topic_id IN(' . implode( ',', $topicIdsToReset ) . ')' ) );
+ }
+
+ if( count( $firstPostIds ) )
+ {
+ \IPS\Db::i()->update( 'forums_posts', array( 'new_topic' => 1 ), array( 'pid IN(' . implode( ',', $firstPostIds ) . ')' ) );
+ }
+
+ if( count( $archivedTopicIdsToReset ) )
+ {
+ \IPS\forums\Topic\ArchivedPost::db()->update( 'forums_archive_posts', array( 'archive_is_first' => 0 ), array( 'archive_topic_id IN(' . implode( ',', $archivedTopicIdsToReset ) . ')' ) );
+ }
+
+ if( count( $firstPostArchivedIds ) )
+ {
+ \IPS\forums\Topic\ArchivedPost::db()->update( 'forums_archive_posts', array( 'archive_is_first' => 1 ), array( 'archive_id IN(' . implode( ',', $firstPostArchivedIds ) . ')' ) );
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack('queue_rebuilding_new_topic_flag'), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildGalleryImages.php b/applications/convert/extensions/core/Queue/RebuildGalleryImages.php
new file mode 100644
index 00000000..f97067af
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildGalleryImages.php
@@ -0,0 +1,107 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 16 Oct 2015
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildGalleryImages
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ try
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(image_id)', 'gallery_images' )->first();
+ }
+ catch( \Exception $e )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int|null New offset or NULL if complete
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ if ( !class_exists( 'IPS\gallery\Image' ) OR !\IPS\Application::appIsEnabled( 'gallery' ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ $last = NULL;
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'gallery_images', array( "image_id>?", $offset ), 'image_id ASC', array( 0, 10 ) ), 'IPS\gallery\Image' ) AS $image )
+ {
+ try
+ {
+ $image->buildThumbnails();
+ $image->save();
+ }
+ catch( \DomainException $e )
+ {
+ $image->delete();
+ }
+ /* File fails the image type test */
+ catch( \InvalidArgumentException $e )
+ {
+ $image->delete();
+ }
+
+ $last = $image->id;
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaning task and percentage complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'queue_rebuilding_gallery_images' ), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildNonContent.php b/applications/convert/extensions/core/Queue/RebuildNonContent.php
new file mode 100644
index 00000000..2353d751
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildNonContent.php
@@ -0,0 +1,118 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 07 Oct 2015
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildNonContent
+{
+ /**
+ * @brief Number of content items to rebuild per cycle
+ */
+ public $rebuild = 50;
+
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ $data['count'] = 0;
+ $_extensionData = explode( '_', $data['extension'] );
+
+ foreach( \IPS\Application::load( $_extensionData[0] )->extensions( 'core', 'EditorLocations' ) as $_key => $extension )
+ {
+ if( $_key != $_extensionData[1] )
+ {
+ continue;
+ }
+
+ if( method_exists( $extension, 'contentCount' ) )
+ {
+ $data['count'] = (int) $extension->contentCount();
+ }
+
+ break;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int|null New offset or NULL if complete
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ foreach( \IPS\Application::allExtensions( 'core', 'EditorLocations', FALSE, NULL, NULL, TRUE ) as $_key => $extension )
+ {
+ if( $_key != $data['extension'] )
+ {
+ continue;
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ $softwareClass = $app->getSource( TRUE, FALSE );
+
+ if( method_exists( $extension, 'rebuildContent' ) )
+ {
+ $did = $extension->rebuildContent( $offset, $softwareClass::fixPostData( $this->rebuild ) );
+ }
+ else
+ {
+ $did = 0;
+ }
+ }
+
+ if( $did == $this->rebuild )
+ {
+ return $offset + $this->rebuild;
+ }
+
+ /* Rebuild is complete */
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaning task and percentage complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'rebuilding_noncontent_posts', FALSE, array( 'sprintf' => \IPS\Member::loggedIn()->language()->addToStack( 'editor__' . $data['extension'] ) ) ), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RebuildTagCache.php b/applications/convert/extensions/core/Queue/RebuildTagCache.php
new file mode 100644
index 00000000..6b904fde
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RebuildTagCache.php
@@ -0,0 +1,163 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 12 October 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RebuildTagCache
+{
+ /**
+ * @brief Number of content items to rebuild per cycle
+ */
+ public $rebuild = 200;
+
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ $classname = $data['class'];
+
+ \IPS\Log::debug( "Getting preQueueData for " . $classname, 'rebuildTagCache' );
+
+ try
+ {
+ $data['count'] = $classname::db()->select( 'MAX(' . $classname::$databasePrefix . $classname::$databaseColumnId . ')', $classname::$databaseTable )->first();
+ $data['realCount'] = $classname::db()->select( 'COUNT(*)', $classname::$databaseTable )->first();
+
+ /* We're going to use the < operator, so we need to ensure the most recent item is rebuilt */
+ $data['runPid'] = $data['count'] + 1;
+ }
+ catch( \Exception $ex )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ \IPS\Log::debug( "PreQueue count for " . $classname . " is " . $data['count'], 'rebuildTagCache' );
+
+ if( $data['count'] == 0 )
+ {
+ return null;
+ }
+
+ $data['indexed'] = 0;
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int|null New offset or NULL if complete
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( &$data, $offset )
+ {
+ $classname = $data['class'];
+ $exploded = explode( '\\', $classname );
+ if ( !class_exists( $classname ) or !\IPS\Application::appIsEnabled( $exploded[1] ) OR !in_array( 'IPS\Content\Tags', class_implements( $classname ) ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Intentionally no try/catch as it means app doesn't exist */
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ \IPS\Log::debug( "Running " . $classname . ", with an offset of " . $offset, 'rebuildTagCache' );
+
+ $select = $classname::db()->select( '*', $classname::$databaseTable, array( array( $classname::$databasePrefix . $classname::$databaseColumnId . ' < ?', $data['runPid'] ) ), $classname::$databasePrefix . $classname::$databaseColumnId . ' DESC', array( 0, $this->rebuild ) );
+ $iterator = new \IPS\Patterns\ActiveRecordIterator( $select, $classname );
+ $last = NULL;
+
+ foreach( $iterator as $item )
+ {
+ $idColumn = $classname::$databaseColumnId;
+
+ /* Is this converted content? */
+ try
+ {
+ /* Just checking, we don't actually need anything */
+ $app->checkLink( $item->$idColumn, $data['link'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $last = $item->$idColumn;
+ continue;
+ }
+
+ $tags = array();
+ $tags['prefix'] = $item->prefix();
+
+ foreach( $item->tags() AS $val )
+ {
+ $tags[] = $val;
+ }
+
+ $item->setTags( $tags, \IPS\Member::load( $item->mapped('author') ) );
+
+ $last = $item->$idColumn;
+
+ $data['indexed']++;
+ }
+
+ /* Store the runPid for the next iteration of this Queue task. This allows the progress bar to show correctly. */
+ $data['runPid'] = $last;
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Return the number rebuilt so far, so that the rebuild progress bar text makes sense */
+ return $data['indexed'];
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ $class = $data['class'];
+ $exploded = explode( '\\', $class );
+ if ( !class_exists( $class ) or !\IPS\Application::appIsEnabled( $exploded[1] ) )
+ {
+ throw new \OutOfRangeException;
+ }
+
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'queue_rebuilding_tag_cache', FALSE, array( 'sprintf' => array( \IPS\Member::loggedIn()->language()->addToStack( $class::$title . '_pl', FALSE, array( 'strtolower' => TRUE ) ) ) ) ), 'complete' => $data['realCount'] ? ( round( 100 / $data['realCount'] * $data['indexed'], 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RecountAnswerRatings.php b/applications/convert/extensions/core/Queue/RecountAnswerRatings.php
new file mode 100644
index 00000000..bce41784
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RecountAnswerRatings.php
@@ -0,0 +1,110 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 19 Nov 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RecountAnswerRatings
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(pid)', 'forums_posts' )->first();
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int New offset
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ if ( !class_exists( 'IPS\forums\Topic\Post' ) OR !\IPS\Application::appisEnabled( 'forums' ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Intentionally no try/catch as it means app doesn't exist */
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'forums_posts', array( "pid>?", $offset ), "pid ASC", array( 0, 200 ) ), 'IPS\forums\Topic\Post' ) AS $answer )
+ {
+ /* Is this converted content? */
+ try
+ {
+ /* Just checking, we don't actually need anything */
+ $app->checkLink( $answer->pid, 'forums_posts' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $last = $answer->pid;
+ continue;
+ }
+
+ /* Rebuild count */
+ $answer->post_field_int = (int) \IPS\Db::i()->select( 'SUM(rating)', 'forums_answer_ratings', array( 'post=?', $answer->pid ) )->first();
+ $answer->save();
+
+ $last = $answer->pid;
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'queue_recounting_answer_ratings' ), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/extensions/core/Queue/RecountQuestionRatings.php b/applications/convert/extensions/core/Queue/RecountQuestionRatings.php
new file mode 100644
index 00000000..7c7802d9
--- /dev/null
+++ b/applications/convert/extensions/core/Queue/RecountQuestionRatings.php
@@ -0,0 +1,110 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 19 Nov 2016
+ */
+
+namespace IPS\convert\extensions\core\Queue;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Background Task
+ */
+class _RecountQuestionRatings
+{
+ /**
+ * Parse data before queuing
+ *
+ * @param array $data
+ * @return array
+ */
+ public function preQueueData( $data )
+ {
+ $data['count'] = \IPS\Db::i()->select( 'count(tid)', 'forums_topics' )->first();
+
+ if( $data['count'] == 0 )
+ {
+ return NULL;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Run Background Task
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return int New offset
+ * @throws \IPS\Task\Queue\OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function run( $data, $offset )
+ {
+ if ( !class_exists( 'IPS\forums\Topic' ) OR !\IPS\Application::appisEnabled( 'forums' ) )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ /* Intentionally no try/catch as it means app doesn't exist */
+ try
+ {
+ $app = \IPS\convert\App::load( $data['app'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'forums_topics', array( "tid>?", $offset ), "tid ASC", array( 0, 200 ) ), 'IPS\forums\Topic' ) AS $question )
+ {
+ /* Is this converted content? */
+ try
+ {
+ /* Just checking, we don't actually need anything */
+ $app->checkLink( $question->tid, 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $last = $question->tid;
+ continue;
+ }
+
+ /* Rebuild count */
+ $question->question_rating = \IPS\Db::i()->select( 'SUM(rating)', 'forums_question_ratings', array( 'topic=?', $question->tid ) )->first();
+ $question->save();
+
+ $last = $question->tid;
+ }
+
+ if( $last === NULL )
+ {
+ throw new \IPS\Task\Queue\OutOfRangeException;
+ }
+
+ return $last;
+ }
+
+ /**
+ * Get Progress
+ *
+ * @param mixed $data Data as it was passed to \IPS\Task::queue()
+ * @param int $offset Offset
+ * @return array( 'text' => 'Doing something...', 'complete' => 50 ) Text explaining task and percentage complete
+ * @throws \OutOfRangeException Indicates offset doesn't exist and thus task is complete
+ */
+ public function getProgress( $data, $offset )
+ {
+ return array( 'text' => \IPS\Member::loggedIn()->language()->addToStack( 'queue_recounting_question_ratings' ), 'complete' => $data['count'] ? ( round( 100 / $data['count'] * $offset, 2 ) ) : 100 );
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/extensions/core/FrontNavigation/index.html b/applications/convert/extensions/core/index.html
similarity index 100%
rename from applications/chat/extensions/core/FrontNavigation/index.html
rename to applications/convert/extensions/core/index.html
diff --git a/applications/chat/extensions/core/GroupForm/index.html b/applications/convert/extensions/index.html
similarity index 100%
rename from applications/chat/extensions/core/GroupForm/index.html
rename to applications/convert/extensions/index.html
diff --git a/applications/convert/hooks/Db.php b/applications/convert/hooks/Db.php
new file mode 100644
index 00000000..674ec6a7
--- /dev/null
+++ b/applications/convert/hooks/Db.php
@@ -0,0 +1,37 @@
+//responsive = FALSE;
+ \IPS\Dispatcher::i()->checkAcpPermission( 'convert_manage' );
+ parent::execute();
+ }
+
+ /**
+ * Show the conversion menu
+ *
+ * @return void
+ */
+ protected function manage()
+ {
+ if ( ! isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/1' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V101/2', 404 );
+ }
+
+ $softwareClass = $app->getSource();
+ $libraryClass = $softwareClass->getLibrary();
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'converting_x_to_x', FALSE, array( 'sprintf' => array( $softwareClass::softwareName(), \IPS\Application::load( $app->sw )->_title ) ) );
+
+ /* Build our table. If I can do this using only the Table helper, I'll be impressed */
+ try
+ {
+ $menuRows = $libraryClass->menuRows();
+ }
+ catch( \IPS\convert\Exception $e )
+ {
+ \IPS\Output::i()->error( $e->getMessage(), '1V101/3' );
+ }
+
+ $table = new \IPS\Helpers\Table\Custom( $menuRows, \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert" ) );
+ $table->rowsTemplate = array( \IPS\Theme::i()->getTemplate( 'table' ), 'convertMenuRow' );
+ $table->extra = array( 'sessionData' => $app->_session, 'appClass' => $app, 'softwareClass' => $softwareClass, 'libraryClass' => $libraryClass, 'menuRows' => $menuRows );
+ $table->mainColumn = 'step_title';
+ $table->showAdvancedSearch = FALSE;
+ $table->noSort = array( 'step_title', 'ips_rows', 'source_rows', 'per_cycle', 'empty_local_data', 'step_method' );
+ $table->include = array( 'step_title', 'ips_rows', 'source_rows', 'per_cycle', 'empty_local_data', 'step_method' );
+ $table->parsers = array(
+ 'step_title' => function( $row )
+ {
+ return \IPS\Member::loggedIn()->language()->addToStack( $row );
+ },
+ );
+
+ \IPS\Output::i()->output = '';
+
+ if ( $softwareClass::canConvertSettings() !== FALSE )
+ {
+ \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'table' )->settingsMessage( $app );
+ }
+
+ \IPS\Output::i()->output .= $table;
+
+ if ( $libraryClass->getPostConversionInformation() != NULL )
+ {
+ \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'table' )->postConversionInformation( $libraryClass->getPostConversionInformation() );
+ }
+ }
+
+ /**
+ * Remove Converted Data
+ *
+ * @return void
+ */
+ protected function emptyData()
+ {
+ if ( ! isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/4' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V101/5' );
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'removing_data' );
+ \IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=emptyData&id={$app->app_id}&method=" . \IPS\Request::i()->method ),
+ function( $data ) use ( $app )
+ {
+ try
+ {
+ return $app->getSource()->getLibrary()->emptyData( $data, \IPS\Request::i()->method );
+ }
+ catch( \IPS\convert\Exception $e )
+ {
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=error&id={$app->app_id}" ) );
+ }
+ },
+ function() use ( $app )
+ {
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$app->app_id}" ) );
+ } );
+ }
+
+ /**
+ * Run a conversion step
+ *
+ * @return void
+ */
+ protected function runStep()
+ {
+ if ( ! isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/6' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V101/7', 404 );
+ }
+
+ /* Do we need more information? */
+ $softwareClass = $app->getSource();
+ if ( ! isset( $app->_session['more_info'][ \IPS\Request::i()->method ] ) OR ( isset( \IPS\Request::i()->reconfigure ) AND \IPS\Request::i()->reconfigure == 1 ) )
+ {
+ if ( in_array( \IPS\Request::i()->method, $softwareClass::checkConf() ) )
+ {
+ $getMoreInfo = $softwareClass->getMoreInfo( \IPS\Request::i()->method );
+ if ( is_array( $getMoreInfo ) AND count( $getMoreInfo ) )
+ {
+ $form = new \IPS\Helpers\Form( \IPS\Request::i()->method . '_more_info', 'continue' );
+ $form->hiddenValues['per_cycle'] = \IPS\Request::i()->per_cycle;
+
+ if ( isset( \IPS\Request::i()->empty_local_data ) AND \IPS\Request::i()->empty_local_data == 1 )
+ {
+ $form->hiddenValues['empty_local_data'] = 1;
+ }
+
+ if ( isset( \IPS\Request::i()->reconfigure ) )
+ {
+ $form->hiddenValues['reconfigure'] = 1;
+ }
+
+ $hints = array();
+ foreach( $getMoreInfo as $key => $input )
+ {
+ $fieldClass = $input['field_class'];
+ $form->add( new $fieldClass( $key, $input['field_default'], $input['field_required'], $input['field_extra'], isset( $input['field_validation'] ) ? $input['field_validation'] : NULL ) );
+ if ( $input['field_hint'] !== NULL )
+ {
+ $form->addMessage( $input['field_hint'], 'ipsMessage ipsMessage_info' );
+ }
+ }
+
+ if ( $values = $form->values() )
+ {
+ $per_cycle = $values['per_cycle'];
+ unset( $values['per_cycle'] );
+
+ $app->saveMoreInfo( \IPS\Request::i()->method, $values );
+ $extra = '';
+ if ( isset( \IPS\Request::i()->empty_local_data ) AND \IPS\Request::i()->empty_local_data == 1 )
+ {
+ $extra .= "&empty_local_data=1";
+ }
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=runStep&id={$app->app_id}&per_cycle={$per_cycle}&method=" . \IPS\Request::i()->method . $extra ) );
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'more_info_needed' );
+ \IPS\Output::i()->output = $form;
+ return;
+ }
+ }
+ }
+
+ /* If we decided to truncate, do so now */
+ if ( isset( \IPS\Request::i()->empty_local_data ) AND \IPS\Request::i()->empty_local_data == 1 )
+ {
+ $app->getSource()->getLibrary()->emptyLocalData( \IPS\Request::i()->method );
+ }
+
+ /* Are we continuing? We need to tell the library that, but we need to use sessions to do it so it only does it on the first cycle. */
+ if ( isset( \IPS\Request::i()->continue ) )
+ {
+ /* Is it set? If not, just go ahead. */
+ if ( !isset( $_SESSION['convertContinue'] ) )
+ {
+ $_SESSION['convertContinue'] = TRUE;
+ }
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'converting' );
+ \IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=runStep&id={$app->app_id}&per_cycle=" . \IPS\Request::i()->per_cycle . "&method=" . \IPS\Request::i()->method ),
+ function( $data ) use ( $app )
+ {
+ try
+ {
+ return $app->getSource()->getLibrary()->process( $data, \IPS\Request::i()->method, \IPS\Request::i()->per_cycle );
+ }
+ catch( \IPS\convert\Exception $e )
+ {
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=error&id={$app->app_id}" ) );
+ }
+ },
+ function() use ( $app )
+ {
+ /* Do we have more steps to process? If so, go to those first. */
+ $canConvert = $app->getSource()->getLibrary()->getConvertableItems();
+ if ( isset( $canConvert[ \IPS\Request::i()->method ]['extra_steps'] ) )
+ {
+ /* See which one we need to run next. */
+ $next = NULL;
+ foreach( $canConvert[ \IPS\Request::i()->method ]['extra_steps'] as $next )
+ {
+ if ( in_array( $next, $app->_session['completed'] ) )
+ {
+ $next = NULL;
+ continue;
+ }
+ }
+
+ /* Found one? */
+ if ( !is_null( $next ) )
+ {
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=runStep&id={$app->app_id}&per_cycle=" . \IPS\Request::i()->per_cycle . "&method={$next}" ) );
+ }
+ }
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$app->app_id}" ) );
+ } );
+ }
+
+ /**
+ * Show an error
+ *
+ * @return void
+ */
+ public function error()
+ {
+ if ( !isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/8' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V101/9', 404 );
+ }
+
+ /* Load the last actual error logged */
+ $error = \IPS\Db::i()->select( '*', 'convert_logs', array( 'log_app=?', $app->app_id ), 'log_id DESC', 1 )->first();
+
+ /* Just use generic error wrapper */
+ \IPS\Output::i()->error( $error['log_method'] . ': ' . $error['log_message'], '2V101/A' );
+ }
+
+ /**
+ * Convert Settings
+ *
+ * @return void
+ */
+ public function settings()
+ {
+ if ( !isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/B' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V101/C', 404 );
+ }
+
+ $softwareClass = $app->getSource();
+
+ if ( $softwareClass::canConvertSettings() === FALSE )
+ {
+ \IPS\Output::i()->error( 'settings_conversion_not_supported', '2V101/D' );
+ }
+
+ $form = new \IPS\Helpers\Form;
+ foreach( $softwareClass->settingsMapList() as $key => $setting )
+ {
+ $value = $setting['value'];
+ if ( is_bool( ( $setting['value'] ) ) )
+ {
+ $value = $setting['value'] === TRUE ? 'On' : 'Off';
+ }
+
+ $form->add( new \IPS\Helpers\Form\Checkbox( $setting['our_key'], TRUE, FALSE, array( 'label' => $value ) ) );
+ }
+
+ if ( $values = $form->values() )
+ {
+ $softwareClass->convertSettings( $values );
+ unset( \IPS\Data\Store::i()->settings );
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$app->app_id}" ), 'saved' );
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'converting_settings' );
+ \IPS\Output::i()->output = $form;
+ }
+
+ /**
+ * Finish
+ *
+ * @return void
+ */
+ public function finish()
+ {
+ if ( !isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V101/E' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\output::i()->error( 'conversion_app_not_found', '2V101/F', 404 );
+ }
+
+ /* Make sure the user confirmed they want to finish */
+ \IPS\Request::i()->confirmedDelete( 'convert_finish_confirm_title', 'convert_finish_confirm', 'finish' );
+
+ $softwareClass = $app->getSource();
+
+ $messages = array();
+
+ /* If we have a parent, run them. */
+ try
+ {
+ if ( $app->parent )
+ {
+ $parent = \IPS\convert\App::load( $app->parent );
+
+ if ( method_exists( $parent->getSource(), 'finish' ) )
+ {
+ $return = $parent->getSource()->finish();
+ $parent->app->log( 'app_finished', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+
+ if ( is_array( $return ) )
+ {
+ $messages = array_merge( $messages, $return );
+ }
+ }
+
+ /* Run siblings if need be */
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'convert_apps', array( "parent=? AND app_id!=?", $parent->app_id, $app->app_id ) ), 'IPS\convert\App' ) AS $sibling )
+ {
+ if ( method_exists( $sibling->getSource(), 'finish' ) )
+ {
+ $return = $sibling->getSource()->finish();
+ $sibling->app->log( 'app_finished', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+
+ if ( is_array( $return ) )
+ {
+ $messages = array_merge( $messages, $return );
+ }
+ }
+ }
+ }
+ else
+ {
+ /* No parent - bubble up to the exception */
+ throw new \OutOfRangeException;
+ }
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* This is a parent - run it's children */
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'convert_apps', array( "parent=?", $app->app_id ) ), 'IPS\convert\App' ) AS $child )
+ {
+ $childSoftwareClass = $child->getSource();
+
+ if ( method_exists( $childSoftwareClass, 'finish' ) )
+ {
+ $return = $childSoftwareClass->finish();
+ $childSoftwareClass->app->log( 'app_finished', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+
+ if ( is_array( $return ) )
+ {
+ $messages = array_merge( $messages, $return );
+ }
+ }
+ }
+ }
+
+ /* And finally, run this one */
+ if ( method_exists( $softwareClass, 'finish' ) )
+ {
+ $return = $softwareClass->finish();
+ $softwareClass->app->log( 'app_finished', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+
+ if ( is_array( $return ) )
+ {
+ $messages = array_merge( $messages, $return );
+ }
+ }
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage" ), implode( ', ', array_map( function( $key ) {
+ return \IPS\Member::loggedIn()->language()->addToStack( $key );
+ }, $messages ) ) );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/modules/admin/manage/create.php b/applications/convert/modules/admin/manage/create.php
new file mode 100644
index 00000000..f0838799
--- /dev/null
+++ b/applications/convert/modules/admin/manage/create.php
@@ -0,0 +1,348 @@
+responsive = FALSE;
+ \IPS\Dispatcher::i()->checkAcpPermission( 'create_manage' );
+ parent::execute();
+ }
+
+ /**
+ * Manage
+ *
+ * @return void
+ */
+ protected function manage()
+ {
+ /* We only use the Wizard helper for starting a conversion, so we need to make sure it is always starting new and not using old session data */
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'convert_start' );
+
+ /* Let's call on the wizard to make some magic */
+ \IPS\Output::i()->output = new \IPS\Helpers\Wizard( array(
+ 'convert_start_application' => array( $this, '_stepOne' ),
+ 'convert_start_source_software' => array( $this, '_stepTwo' ),
+ 'convert_start_database_details' => array( $this, '_stepThree' ),
+ ), \IPS\Http\Url::internal( 'app=convert&module=manage&controller=create' ) );
+ }
+
+ /**
+ * First step of conversion
+ *
+ * @return void
+ */
+ public function _stepOne( $data )
+ {
+ foreach( \IPS\Application::applications() AS $app )
+ {
+ if ( \IPS\Member::loggedIn()->language()->checkKeyExists( "{$app->directory}_node_select" ) )
+ {
+ \IPS\member::loggedIn()->language()->words["__app_{$app->directory}"] = \IPS\Member::loggedIn()->language()->addToStack( "{$app->directory}_node_select" );
+ }
+ }
+
+ /* Have we done any conversions yet? */
+ $appCount = \IPS\Db::i()->select( 'COUNT(*)', 'convert_apps', array( "parent=?", 0 ) )->first();
+
+ $form = new \IPS\Helpers\Form( 'convert_application', 'select_source' );
+ $form->add( new \IPS\Helpers\Form\Node( 'convert_start_application', NULL, TRUE, array( 'class' => 'IPS\\Application', 'subnodes' => FALSE, 'permissionCheck' => function( $node ) use ( $appCount )
+ {
+ foreach( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/convert/sources/Library' ) AS $file )
+ {
+ if ( ucwords( $node->directory ) === str_replace( '.php', '', $file->getFilename() ) )
+ {
+ if ( $node->directory === 'core' OR $appCount > 0 )
+ {
+ return TRUE;
+ }
+ }
+ }
+
+ return NULL;
+ } ) ) );
+
+ if ( $values = $form->values() )
+ {
+ return array( 'convert_start_application' => $values['convert_start_application']->directory );
+ }
+
+ return $form;
+ }
+
+ /**
+ * Second step of conversion
+ *
+ * @return void
+ */
+ public function _stepTwo( $data )
+ {
+ $options = array();
+ foreach( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/convert/sources/Software/' . ucwords( $data['convert_start_application'] ) ) AS $file )
+ {
+ $classname = 'IPS\\convert\\Software\\' . ucwords( $data['convert_start_application'] ) . '\\' . ucwords( str_replace( '.php', '', $file->getFilename() ) );
+ if ( class_exists( $classname ) AND $classname::canConvert() !== NULL )
+ {
+ $options[ $classname::softwareKey() ] = $classname::softwareName();
+ }
+ }
+
+ $form = new \IPS\Helpers\Form( 'convert_source_software', 'enter_database_details' );
+ $form->add( new \IPS\Helpers\Form\Select( 'convert_start_source_software', NULL, TRUE, array( 'options' => $options ) ) );
+
+ if ( $values = $form->values() )
+ {
+ return $values;
+ }
+
+ return $form;
+ }
+
+ /**
+ * Third step of conversion
+ *
+ * @return void
+ */
+ public function _stepThree( $data )
+ {
+ $form = new \IPS\Helpers\Form( 'convert_database_details', 'start_conversion' );
+ $classname = 'IPS\\convert\\Software\\' . ucwords( $data['convert_start_application'] ) . '\\' . ucwords( $data['convert_start_source_software'] );
+
+ if ( !is_null( $classname::parents() ) AND is_array( $classname::parents() ) )
+ {
+ /* Do we have any available parents? */
+ $where = array();
+ $options = array();
+ foreach( $classname::parents() AS $app => $sw )
+ {
+ foreach( $sw AS $key )
+ {
+ $where[] = array( 'sw=? AND app_key=?', $app, $key );
+ }
+ }
+
+ try
+ {
+ foreach( \IPS\Db::i()->select( '*', 'convert_apps', $where ) AS $row )
+ {
+ $name = explode( '_', $row['name'] );
+ $parentClassname = 'IPS\\convert\\Software\\' . ucwords( $name[0] ) . '\\' . ucwords( $name[1] );
+ $options[$row['app_id']] = $parentClassname::softwareName();
+ }
+ }
+ catch( \Exception $e ) {}
+
+ /* If we have any possible parents, lets add a select menu */
+ if ( count( $options ) > 0 )
+ {
+ $form->addHeader( 'convert_start_select_parent' );
+ $form->add( new \IPS\Helpers\Form\Select( 'convert_start_parent', NULL, FALSE, array( 'options' => $options ) ) );
+ $form->addHeader( 'convert_start_or_database' );
+ }
+
+ /* This software class requires a parent, but we don't have any that have been converted */
+ elseif( $classname::requiresParent() )
+ {
+ \IPS\Output::i()->error( 'convert_no_applicable_parent', '4V346/1' );
+ }
+ }
+ if ( $classname::getPreConversionInformation() !== NULL )
+ {
+ $form->addMessage( \IPS\Member::loggedIn()->language()->addToStack( $classname::getPreConversionInformation() ), '', FALSE );
+ }
+
+ $form->add( new \IPS\Helpers\Form\Text( 'convert_start_database_host', NULL, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Number( 'convert_start_database_port', 3306, FALSE, array( 'max' => 65535, 'min' => 1 ) ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'convert_start_database_name', NULL, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'convert_start_database_user', NULL, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Password( 'convert_start_database_pass', NULL, FALSE ) );
+
+ /* If the source software doesn't use a prefix, then we should not ask for one to avoid confusion. */
+ if ( $classname::usesPrefix() )
+ {
+ $form->add( new \IPS\Helpers\Form\Text( 'convert_start_database_prefix', NULL, FALSE ) );
+ }
+
+ $form->add( new \IPS\Helpers\Form\Text( 'convert_start_database_charset', NULL, FALSE, array( 'placeholder' => 'utf8' ) ) );
+
+ if ( $values = $form->values() )
+ {
+ /* Did we select a parent but no DB details? */
+ if (
+ isset( $values['convert_start_parent'] ) AND
+ ( !isset( $values['convert_start_database_host'] ) || empty($values['convert_start_database_host']) ) AND
+ ( !isset( $values['convert_start_database_name'] ) || empty($values['convert_start_database_name']) ) AND
+ ( !isset( $values['convert_start_database_user'] ) || empty($values['convert_start_database_user']) ) AND
+ ( !isset( $values['convert_start_database_pass'] ) || empty($values['convert_start_database_pass']) ) AND
+ ( !isset( $values['convert_start_database_prefix'] ) || empty($values['convert_start_database_prefix']) ) AND
+ ( !isset( $values['convert_start_database_charset'] ) || empty($values['convert_start_database_charset']) )
+ )
+ {
+ /* We did - load the parent app and store the DB details for that */
+ $parentApp = \IPS\convert\App::load( $values['convert_start_parent'] );
+ $values['convert_start_database_host'] = $parentApp->db_host;
+ $values['convert_start_database_port'] = $parentApp->db_port;
+ $values['convert_start_database_name'] = $parentApp->db_db;
+ $values['convert_start_database_user'] = $parentApp->db_user;
+ $values['convert_start_database_pass'] = $parentApp->db_pass;
+ $values['convert_start_database_prefix'] = $parentApp->db_prefix;
+ $values['convert_start_database_charset'] = $parentApp->db_charset;
+ }
+
+ /* Start the app object */
+ $app = new \IPS\convert\App;
+ $app->sw = $data['convert_start_application'];
+ $app->app_key = $data['convert_start_source_software'];
+
+ try
+ {
+ /* Test the connection */
+ $connectionSettings = array(
+ 'sql_host' => $values['convert_start_database_host'],
+ 'sql_port' => $values['convert_start_database_port'],
+ 'sql_user' => $values['convert_start_database_user'],
+ 'sql_pass' => $values['convert_start_database_pass'],
+ 'sql_database' => $values['convert_start_database_name'],
+ 'sql_tbl_prefix' => $values['convert_start_database_prefix'],
+ );
+
+ if ( $values['convert_start_database_charset'] === 'utf8mb4' )
+ {
+ $connectionSettings['sql_utf8mb4'] = TRUE;
+ }
+
+ $db = \IPS\Db::i( 'convertertest', $connectionSettings );
+
+ /* Now test the charset */
+ if ( $values['convert_start_database_charset'] AND !in_array( $values['convert_start_database_charset'], array( 'utf8', 'utf8mb4' ) ) )
+ {
+ /* Get all db charsets and make sure the one we entered is valid */
+ $charsets = \IPS\convert\Software::getDatabaseCharsets( $db );
+
+ if ( !in_array( mb_strtolower( $values['convert_start_database_charset'] ), $charsets ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_charset' );
+ }
+
+ $db->set_charset( $values['convert_start_database_charset'] );
+ }
+
+ /* Try to verify that the db prefix is correct */
+ $appClassName = $app->getSource( FALSE, FALSE );
+ $canConvert = $appClassName::canConvert();
+ $appClass = new $appClassName( $app, FALSE );
+ $appClass->db = $db;
+
+ $testAgainst = array_shift( $canConvert );
+
+ /* We specifically use CountRows to test this, canConvert() may contain references to tables that do not exist..
+ * ..This is for the row count look up.
+ */
+ try
+ {
+ $appClass->countRows( $testAgainst['table'] );
+ }
+ catch( \UnderflowException $e )
+ {
+ throw new \InvalidArgumentException( 'invalid_prefix' );
+ }
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ if( $e->getMessage() == 'invalid_charset' )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db_charset');
+ return $form;
+ }
+ else if( $e->getMessage() == 'invalid_prefix' )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db_prefix');
+ return $form;
+ }
+ else
+ {
+ throw $e;
+ }
+ }
+ catch( \Exception $e )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db');
+ return $form;
+ }
+
+ try
+ {
+ $appExists = \IPS\convert\App::load( $data['convert_start_application'] . '_' . $data['convert_start_source_software'], 'name' );
+ $app->name = $data['convert_start_application'] . '_' . $data['convert_start_source_software'] . '_' . time();
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $app->name = $data['convert_start_application'] . '_' . $data['convert_start_source_software'];
+ }
+
+ if ( isset( $values['convert_start_parent'] ) )
+ {
+ $app->parent = $values['convert_start_parent'];
+ }
+ else
+ {
+ $app->parent = 0;
+ }
+
+ if ( !isset( $values['convert_start_database_prefix'] ) )
+ {
+ $values['convert_start_database_prefix'] = '';
+ }
+
+ $app->db_host = $values['convert_start_database_host'];
+ $app->db_port = $values['convert_start_database_port'];
+ $app->db_db = $values['convert_start_database_name'];
+ $app->db_user = $values['convert_start_database_user'];
+ $app->db_pass = $values['convert_start_database_pass'];
+ $app->db_prefix = $values['convert_start_database_prefix'];
+
+ /* Check for existing apps */
+ try
+ {
+ $existingApp = \IPS\DB::i()->select( '*', 'convert_apps', array( 'sw=? AND app_key=? AND db_host=? AND db_db=? AND db_user=? AND db_pass=? AND db_prefix=?', $app->sw, $app->app_key, $app->db_host, $app->db_db, $app->db_user, $app->db_pass, $app->db_prefix ) )->first();
+
+ /* App already exists */
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$existingApp['app_id']}" ), 'conversion_app_already_exists' );
+ }
+ catch( \UnderflowException $e ) { }
+
+ if ( !empty( $values['convert_start_database_charset'] ) )
+ {
+ $app->db_charset = $values['convert_start_database_charset'];
+ }
+ else
+ {
+ $app->db_charset = 'utf8';
+ }
+
+ $app->save();
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$app->app_id}" ) );
+ }
+
+ return $form;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/modules/admin/manage/manage.php b/applications/convert/modules/admin/manage/manage.php
new file mode 100644
index 00000000..b0504062
--- /dev/null
+++ b/applications/convert/modules/admin/manage/manage.php
@@ -0,0 +1,417 @@
+responsive = FALSE;
+ \IPS\Dispatcher::i()->checkAcpPermission( 'manage_manage' );
+ parent::execute();
+ }
+
+ /**
+ * Manage
+ *
+ * @return void
+ */
+ protected function manage()
+ {
+ /* If mod_rewrite is not enabled, advise the administrator that old URLs will not redirect to the new locations automatically */
+ if( !\IPS\Settings::i()->htaccess_mod_rewrite )
+ {
+ \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core' )->message( 'convert_mod_rewrite_urls', 'warning' );
+ }
+
+ /* Create the table */
+ $table = new \IPS\Helpers\Table\Db( 'convert_apps', \IPS\Http\Url::internal( 'app=convert&module=manage&controller=manage' ) );
+ $table->langPrefix = 'convert_';
+ $table->include = array( 'sw', 'app_key', 'name', 'parent' );
+
+ $table->parsers = array(
+ 'sw' => function( $val, $row )
+ {
+ switch( $val )
+ {
+ case 'board':
+ $val = 'forums';
+ break;
+
+ case 'ccs':
+ $val = 'cms';
+ break;
+ }
+
+ try
+ {
+ return \IPS\Application::load( $val )->_title;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ return ucwords( $val );
+ }
+ },
+ 'app_key' => function( $val, $row )
+ {
+ $app = \IPS\convert\App::constructFromData( $row );
+ try
+ {
+ $classname = get_class( $app->getSource() );
+ return $classname::softwareName();
+ }
+ catch( \Exception $ex )
+ {
+ return $app->name;
+ }
+ },
+ 'parent' => function( $val, $row )
+ {
+ try
+ {
+ if ( ! $val )
+ {
+ return \IPS\Member::loggedIn()->language()->addToStack( 'convert_no_parent' );
+ }
+ else
+ {
+ return \IPS\convert\App::load( $val )->name;
+ }
+ }
+ catch( \OutOfRangeException $e )
+ {
+ return \IPS\Member::loggedIn()->language()->addToStack( 'convert_cannot_parent' );
+ }
+ }
+ );
+
+ $table->rootButtons = array(
+ 'start' => array(
+ 'icon' => 'plus',
+ 'title' => 'convert_start',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=create&_new=1" ),
+ )
+ );
+
+ if ( \IPS\IN_DEV )
+ {
+ $table->rootButtons += array(
+ 'software' => array(
+ 'icon' => 'plus',
+ 'title' => 'new_software',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage&do=software" ),
+ 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('new_software') )
+ ),
+ 'library' => array(
+ 'icon' => 'plus',
+ 'title' => 'new_library',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage&do=library" ),
+ 'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('new_library') )
+ )
+ );
+ }
+
+ $table->rowButtons = function( $row )
+ {
+ try
+ {
+ /* Try to load the application to make sure it's installed */
+ \IPS\Application::load( $row['sw'] );
+
+ /* Try to load the app class - if exception, the converter no longer exists */
+ get_class( \IPS\convert\App::constructFromData( $row )->getSource() );
+
+ $return = array();
+
+ $return[] = array(
+ 'icon' => 'chevron-circle-right',
+ 'title' => 'continue',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&id={$row['app_id']}" )
+ );
+
+ if ( $row['parent'] == 0 )
+ {
+ $return[] = array(
+ 'icon' => 'check',
+ 'title' => 'finish',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=finish&id={$row['app_id']}" ),
+ 'data' => array(
+ 'confirm' => '', 'confirmSubMessage' => \IPS\Member::loggedIn()->language()->get( 'convert_finish_confirm' )
+ )
+ );
+ }
+
+ $return[] = array(
+ 'icon' => 'pencil',
+ 'title' => 'edit',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage&do=edit&id={$row['app_id']}" ),
+ );
+
+ $return[] = array(
+ 'icon' => 'times-circle',
+ 'title' => 'delete',
+ 'link' => \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage&do=delete&id={$row['app_id']}" ),
+ 'data' => array( 'delete' => '' ),
+ );
+
+ return $return;
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ return array();
+ }
+ /* Application has been uninstalled */
+ catch( \OutOfRangeException $e )
+ {
+ return array();
+ }
+ };
+
+ /* Display */
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'menu__convert_manage' );
+ \IPS\Output::i()->output .= \IPS\Theme::i()->getTemplate( 'global', 'core' )->block( 'title', (string) $table );
+ }
+
+ /**
+ * Create New Converter
+ *
+ * @return void
+ */
+ public function software()
+ {
+ if ( \IPS\IN_DEV === FALSE )
+ {
+ \IPS\Output::i()->error( 'new_sofware_not_in_dev', '1V100/1', 403 );
+ }
+
+ $form = new \IPS\Helpers\Form;
+ $form->add( new \IPS\Helpers\Form\Text( 'classname', NULL, TRUE ) );
+
+ $apps = array();
+ foreach( new \DirectoryIterator( \IPS\ROOT_PATH . '/applications/convert/sources/Library' ) as $file )
+ {
+ if ( !$file->isDir() AND !$file->isDot() AND ( \substr( $file->getFilename(), -4 ) == '.php' ) AND $file->getFilename() != 'Board.php' AND $file->getFilename() != 'Ccs.php' )
+ {
+ $apps[ str_replace( '.php', '', $file->getFilename() ) ] = str_replace( '.php', '', $file->getFilename() );
+ }
+ }
+
+ $form->add( new \IPS\Helpers\Form\Select( 'library', NULL, FALSE, array( 'options' => $apps ) ) );
+
+ if ( $values = $form->values() )
+ {
+ $default = file_get_contents( \IPS\ROOT_PATH . '/applications/convert/data/defaults/Software.txt' );
+ $code = str_replace( array( '<#CLASS#>', '<#APP#>', '<#CLASS_LOWER#>' ), array( $values['classname'], $values['library'], mb_strtolower( $values['classname'] ) ), $default );
+ \file_put_contents( \IPS\ROOT_PATH . '/applications/convert/sources/Software/' . $values['library'] . '/' . $values['classname'] . '.php', $code );
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage" ), 'saved' );
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'new_software' );
+ \IPS\Output::i()->output = (string) $form;
+ }
+
+ /**
+ * Create New Library
+ *
+ * @return void
+ */
+ public function library()
+ {
+ if ( \IPS\IN_DEV === FALSE )
+ {
+ \IPS\Output::i()->error( 'new_library_not_in_dev', '1V100/2', 403 );
+ }
+
+ $form = new \IPS\Helpers\Form;
+ $form->add( new \IPS\Helpers\Form\Text( 'classname', NULL, TRUE ) );
+
+ if ( $values = $form->values() )
+ {
+ $default = file_get_contents( \IPS\ROOT_PATH . '/applications/convert/data/defaults/Library.txt' );
+ $code = str_replace( array( '<#CLASS#>', '<#CLASS_LOWER#>' ), array( $values['classname'], mb_strtolower( $values['classname'] ) ), $default );
+ \file_put_contents( \IPS\ROOT_PATH . '/applications/convert/sources/Library/' . $values['classname'] . '.php', $code );
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=convert&module=manage&controller=manage' ), 'saved' );
+ }
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'new_library' );
+ \IPS\Output::i()->output = (string) $form;
+ }
+
+ /**
+ * Edit a conversion
+ *
+ * @return void
+ */
+ public function edit()
+ {
+ if ( !isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V100/3' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V100/4', 404 );
+ }
+
+ $classname = $app->getSource( FALSE );
+
+ $form = new \IPS\Helpers\Form;
+
+ if ( $classname::getPreConversionInformation() !== NULL )
+ {
+ $form->addMessage( \IPS\Member::loggedIn()->language()->addToStack( $classname::getPreConversionInformation() ), '', FALSE );
+ }
+
+ $form->add( new \IPS\Helpers\Form\Text( 'db_host', $app->db_host, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Number( 'db_port', $app->db_port, FALSE, array( 'max' => 65535, 'min' => 1 ) ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'db_db', $app->db_db, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'db_user', $app->db_user, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Password( 'db_pass', $app->db_pass, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'db_prefix', $app->db_prefix, FALSE ) );
+ $form->add( new \IPS\Helpers\Form\Text( 'db_charset', $app->db_charset, FALSE ) );
+
+ \IPS\Output::i()->title = \IPS\Member::loggedIn()->language()->addToStack( 'edit_conversion' );
+
+ if ( $values = $form->values() )
+ {
+ try
+ {
+ /* Test the connection */
+ $connectionSettings = array(
+ 'sql_host' => $values['db_host'],
+ 'sql_port' => $values['db_port'],
+ 'sql_user' => $values['db_user'],
+ 'sql_pass' => $values['db_pass'],
+ 'sql_database' => $values['db_db'],
+ 'sql_tbl_prefix' => $values['db_prefix'],
+ );
+
+ if ( $values['db_charset'] === 'utf8mb4' )
+ {
+ $connectionSettings['sql_utf8mb4'] = TRUE;
+ }
+
+ $db = \IPS\Db::i( 'convertertest', $connectionSettings );
+
+ /* Now test the charset */
+ if ( $values['db_charset'] AND !in_array( $values['db_charset'], array( 'utf8', 'utf8mb4' ) ) )
+ {
+ /* Get all db charsets and make sure the one we entered is valid */
+ $charsets = \IPS\convert\Software::getDatabaseCharsets( $db );
+
+ if ( !in_array( mb_strtolower( $values['db_charset'] ), $charsets ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_charset' );
+ }
+
+ $db->set_charset( $values['db_charset'] );
+ }
+
+ /* Try to verify that the db prefix is correct */
+ $appClass = $app->getSource( FALSE, FALSE );
+ $canConvert = $appClass::canConvert();
+
+ $testAgainst = array_shift( $canConvert );
+
+ if( !$db->checkForTable( $testAgainst['table'] ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_prefix' );
+ }
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ if( $e->getMessage() == 'invalid_charset' )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db_charset');
+ \IPS\Output::i()->output = $form;
+ return;
+ }
+ else if( $e->getMessage() == 'invalid_prefix' )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db_prefix');
+ \IPS\Output::i()->output = $form;
+ return;
+ }
+ else
+ {
+ throw $e;
+ }
+ }
+ catch( \Exception $e )
+ {
+ $form->error = \IPS\Member::loggedIn()->language()->addToStack('convert_cant_connect_db');
+ \IPS\Output::i()->output = $form;
+ return;
+ }
+
+ $app->db_host = $values['db_host'];
+ $app->db_port = $values['db_port'];
+ $app->db_db = $values['db_db'];
+ $app->db_user = $values['db_user'];
+ $app->db_pass = $values['db_pass'];
+ $app->db_prefix = $values['db_prefix'];
+ $app->db_charset = $values['db_charset'];
+ $app->save();
+
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( "app=convert&module=manage&controller=manage" ), 'saved' );
+ }
+
+ \IPS\Output::i()->output = $form;
+ }
+
+ /**
+ * Delete a Conversion
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ if ( !isset( \IPS\Request::i()->id ) )
+ {
+ \IPS\Output::i()->error( 'no_conversion_app', '2V100/5' );
+ }
+
+ try
+ {
+ $app = \IPS\convert\App::load( \IPS\Request::i()->id );
+
+ /* Check for children */
+ try
+ {
+ \IPS\DB::i()->select( '*', 'convert_apps', array( 'parent=?', $app->app_id ) )->first();
+ \IPS\Output::i()->error( 'conversion_app_has_children', '2V100/6' );
+ }
+ catch( \UnderflowException $ex ) { }
+
+ $app->delete();
+ \IPS\Output::i()->redirect( \IPS\Http\Url::internal( 'app=convert&module=manage&controller=manage' ), 'conversion_deleted' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Output::i()->error( 'conversion_app_not_found', '2V100/6', 404 );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/extensions/index.html b/applications/convert/modules/front/index.html
similarity index 100%
rename from applications/chat/extensions/index.html
rename to applications/convert/modules/front/index.html
diff --git a/applications/chat/hooks/index.html b/applications/convert/modules/index.html
similarity index 100%
rename from applications/chat/hooks/index.html
rename to applications/convert/modules/index.html
diff --git a/applications/chat/index.html b/applications/convert/setup/index.html
similarity index 100%
rename from applications/chat/index.html
rename to applications/convert/setup/index.html
diff --git a/applications/convert/setup/install/queries.json b/applications/convert/setup/install/queries.json
new file mode 100644
index 00000000..8d77d346
--- /dev/null
+++ b/applications/convert/setup/install/queries.json
@@ -0,0 +1,28 @@
+{
+ "1": {
+ "method": "addColumn",
+ "params": [
+ "core_members",
+ {
+ "name": "conv_password",
+ "type": "VARCHAR",
+ "length": 255,
+ "null": true,
+ "default": null
+ }
+ ]
+ },
+ "2": {
+ "method": "addColumn",
+ "params": [
+ "core_members",
+ {
+ "name": "conv_password_extra",
+ "type": "VARCHAR",
+ "length": 255,
+ "null": true,
+ "default": null
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/interface/index.html b/applications/convert/setup/upg_100000/index.html
similarity index 100%
rename from applications/chat/interface/index.html
rename to applications/convert/setup/upg_100000/index.html
diff --git a/applications/convert/setup/upg_100000/queries.json b/applications/convert/setup/upg_100000/queries.json
new file mode 100644
index 00000000..476e97f5
--- /dev/null
+++ b/applications/convert/setup/upg_100000/queries.json
@@ -0,0 +1,189 @@
+{
+ "1": {
+ "method": "renameTable",
+ "params": [
+ "conv_apps",
+ "convert_apps"
+ ]
+ },
+ "2": {
+ "method": "renameTable",
+ "params": [
+ "conv_link",
+ "convert_link"
+ ]
+ },
+ "3": {
+ "method": "renameTable",
+ "params": [
+ "conv_link_pms",
+ "convert_link_pms"
+ ]
+ },
+ "4": {
+ "method": "renameTable",
+ "params": [
+ "conv_link_topics",
+ "convert_link_topics"
+ ]
+ },
+ "5": {
+ "method": "renameTable",
+ "params": [
+ "conv_link_posts",
+ "convert_link_posts"
+ ]
+ },
+ "6": {
+ "method": "createTable",
+ "params": [
+ {
+ "name": "convert_logs",
+ "columns": {
+ "log_id": {
+ "name": "log_id",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": null,
+ "comment": "ID Number",
+ "unsigned": true,
+ "zerofill": false,
+ "auto_increment": true,
+ "binary": false
+ },
+ "log_message": {
+ "name": "log_message",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Log Message",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_app": {
+ "name": "log_app",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "The conversion app.",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_severity": {
+ "name": "log_severity",
+ "type": "TINYINT",
+ "length": 1,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "Flag to indicate the type of log (notice, warning, error)",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "log_method": {
+ "name": "log_method",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Conversion step that triggered this log",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "columns": [
+ "log_id"
+ ],
+ "length": [
+ null
+ ]
+ },
+ "log_app": {
+ "type": "key",
+ "name": "log_app",
+ "columns": [
+ "log_app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ }
+ }
+ ]
+ },
+ "7": {
+ "method": "createTable",
+ "params": [
+ {
+ "name": "convert_app_sessions",
+ "columns": {
+ "session_app_id": {
+ "name": "session_app_id",
+ "type": "BIGINT",
+ "length": 20,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "0",
+ "comment": "ID Number",
+ "unsigned": true,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ },
+ "session_app_data": {
+ "name": "session_app_data",
+ "type": "MEDIUMTEXT",
+ "length": null,
+ "decimals": null,
+ "values": [],
+ "allow_null": true,
+ "default": null,
+ "comment": "Session Data containing rows converted, completed steps, etc.",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ },
+ "indexes": {
+ "PRIMARY": {
+ "type": "primary",
+ "name": "PRIMARY",
+ "columns": [
+ "session_app_id"
+ ],
+ "length": [
+ null
+ ]
+ }
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_100000/upgrade.php b/applications/convert/setup/upg_100000/upgrade.php
new file mode 100644
index 00000000..e304129f
--- /dev/null
+++ b/applications/convert/setup/upg_100000/upgrade.php
@@ -0,0 +1,45 @@
+checkForColumn( 'core_members', 'conv_password' ) )
+ {
+ \IPS\Db::i()->changeColumn( 'core_members', 'conv_password', array( 'name' => 'conv_password', 'type' => 'VARCHAR', 'length' => 255, 'null' => TRUE, 'default' => NULL ) );
+ }
+ else
+ {
+ \IPS\Db::i()->addColumn( 'core_members', array( 'name' => 'conv_password', 'type' => 'VARCHAR', 'length' => 255, 'null' => TRUE, 'default' => NULL ) );
+ }
+
+ if ( \IPS\Db::i()->checkForColumn( 'core_members', 'misc' ) )
+ {
+ \IPS\Db::i()->changeColumn( 'core_members', 'misc', array( 'name' => 'conv_password_extra', 'type' => 'VARCHAR', 'length' => 255, 'null' => TRUE, 'default' => NULL ) );
+ }
+ else
+ {
+ \IPS\Db::i()->addColumn( 'core_members', array( 'name' => 'conv_password_extra', 'type' => 'VARCHAR', 'length' => 255, 'null' => TRUE, 'default' => NULL ) );
+ }
+
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101002/queries.json b/applications/convert/setup/upg_101002/queries.json
new file mode 100644
index 00000000..defb2ce5
--- /dev/null
+++ b/applications/convert/setup/upg_101002/queries.json
@@ -0,0 +1,22 @@
+{
+ "1": {
+ "method": "addColumn",
+ "params": [
+ "convert_apps",
+ {
+ "name": "start_date",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "0",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101003/upgrade.php b/applications/convert/setup/upg_101003/upgrade.php
new file mode 100644
index 00000000..f5406bd8
--- /dev/null
+++ b/applications/convert/setup/upg_101003/upgrade.php
@@ -0,0 +1,96 @@
+ array(
+ array(
+ 'table' => 'downloads_categories',
+ 'old' => 'conv_parent',
+ 'new' => 'cconv_parent'
+ )
+ ),
+ 'gallery' => array(
+ array(
+ 'table' => 'gallery_categories',
+ 'old' => 'conv_parent',
+ 'new' => 'category_conv_parent'
+ )
+ ),
+ 'cms' => array(
+ array(
+ 'table' => 'cms_database_categories',
+ 'old' => 'conv_parent',
+ 'new' => 'category_conv_parent',
+ )
+ ),
+ 'nexus' => array(
+ array(
+ 'table' => 'nexus_package_groups',
+ 'old' => 'conv_pg_parent',
+ 'new' => 'pg_conv_parent'
+ ),
+ array(
+ 'table' => 'nexus_packages',
+ 'old' => 'conv_p_associable',
+ 'new' => 'p_conv_associable'
+ ),
+ array(
+ 'table' => 'nexus_purchases',
+ 'old' => 'conv_ps_parent',
+ 'new' => 'ps_conv_parent'
+ )
+ )
+ );
+
+ foreach( $old AS $app => $tables )
+ {
+ foreach( $tables AS $data )
+ {
+ /* Does the table and old column exist? */
+ if ( \IPS\Db::i()->checkForTable( $data['table'] ) AND \IPS\Db::i()->checkForColumn( $data['table'], $data['old'] ) )
+ {
+ /* Does the new column exist? */
+ if ( \IPS\Db::i()->checkForColumn( $data['table'], $data['new'] ) )
+ {
+ /* Drop it */
+ \IPS\Db::i()->dropColumn( $data['table'], $data['new'] );
+ }
+
+ /* Alter the old one */
+ \IPS\Db::i()->changeColumn( $data['table'], $data['old'], array(
+ 'name' => $data['new'],
+ 'type' => 'BIGINT',
+ 'length' => 20,
+ 'default' => 0
+ ) );
+ }
+ }
+ }
+
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101004/queries.json b/applications/convert/setup/upg_101004/queries.json
new file mode 100644
index 00000000..988c6560
--- /dev/null
+++ b/applications/convert/setup/upg_101004/queries.json
@@ -0,0 +1,22 @@
+{
+ "1": {
+ "method": "addColumn",
+ "params": [
+ "convert_logs",
+ {
+ "name": "log_item_id",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": null,
+ "allow_null": true,
+ "default": null,
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101006/queries.json b/applications/convert/setup/upg_101006/queries.json
new file mode 100644
index 00000000..825dd5b0
--- /dev/null
+++ b/applications/convert/setup/upg_101006/queries.json
@@ -0,0 +1,369 @@
+{
+ "1": {
+ "method": "changeColumn",
+ "params": [
+ "convert_link_posts",
+ "foreign_id",
+ {
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ },
+ "2": {
+ "method": "changeColumn",
+ "params": [
+ "convert_link_pms",
+ "foreign_id",
+ {
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ },
+ "3": {
+ "method": "changeColumn",
+ "params": [
+ "convert_link_topics",
+ "foreign_id",
+ {
+ "name": "foreign_id",
+ "type": "VARCHAR",
+ "length": 255,
+ "decimals": null,
+ "values": [],
+ "allow_null": false,
+ "default": "",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ },
+ "4": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_posts",
+ {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ }
+ ]
+ },
+ "5": {
+ "method": "changeIndex",
+ "params": [
+ "convert_link_posts",
+ "ipb_id",
+ {
+ "type": "key",
+ "name": "ipb_id",
+ "columns": [
+ "ipb_id"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "6": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_posts",
+ "ipb_id"
+ ]
+ },
+ "7": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_posts",
+ "type"
+ ]
+ },
+ "8": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_topics",
+ "ipb_id"
+ ]
+ },
+ "9": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_topics",
+ "type"
+ ]
+ },
+ "10": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_topics",
+ {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ }
+ ]
+ },
+ "11": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_pms",
+ "ipb_id"
+ ]
+ },
+ "12": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link_pms",
+ "type"
+ ]
+ },
+ "13": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_pms",
+ {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ }
+ ]
+ },
+ "14": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link",
+ "foreign_id"
+ ]
+ },
+ "15": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link",
+ "ipb_id"
+ ]
+ },
+ "16": {
+ "method": "dropIndex",
+ "params": [
+ "convert_link",
+ "type"
+ ]
+ },
+ "17": {
+ "method": "addIndex",
+ "params": [
+ "convert_link",
+ {
+ "type": "key",
+ "name": "foreign_id",
+ "columns": [
+ "foreign_id"
+ ],
+ "length": [
+ 250
+ ]
+ }
+ ]
+ },
+ "18": {
+ "method": "addIndex",
+ "params": [
+ "convert_link",
+ {
+ "type": "key",
+ "name": "local_lookup",
+ "columns": [
+ "ipb_id",
+ "type",
+ "app"
+ ],
+ "length": [
+ null,
+ null,
+ null
+ ]
+ }
+ ]
+ },
+ "19": {
+ "method": "addIndex",
+ "params": [
+ "convert_link",
+ {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "20": {
+ "method": "addIndex",
+ "params": [
+ "convert_link",
+ {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "21": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_pms",
+ {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "22": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_pms",
+ {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "23": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_posts",
+ {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "24": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_posts",
+ {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "25": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_topics",
+ {
+ "type": "key",
+ "name": "type",
+ "columns": [
+ "type"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ },
+ "26": {
+ "method": "addIndex",
+ "params": [
+ "convert_link_topics",
+ {
+ "type": "key",
+ "name": "app",
+ "columns": [
+ "app"
+ ],
+ "length": [
+ null
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101007/queries.json b/applications/convert/setup/upg_101007/queries.json
new file mode 100644
index 00000000..0637a088
--- /dev/null
+++ b/applications/convert/setup/upg_101007/queries.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101007/upgrade.php b/applications/convert/setup/upg_101007/upgrade.php
new file mode 100644
index 00000000..a6c92121
--- /dev/null
+++ b/applications/convert/setup/upg_101007/upgrade.php
@@ -0,0 +1,435 @@
+checkForTable( 'custom_bbcode' ) )
+ {
+ \IPS\Db::i()->renameTable( 'custom_bbcode', 'convert_custom_bbcode' );
+ }
+ else
+ {
+ \IPS\Db::i()->createTable( array (
+ 'name' => 'convert_custom_bbcode',
+ 'columns' => array(
+ 'bbcode_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 10,
+ 'name' => 'bbcode_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_title' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 255,
+ 'name' => 'bbcode_title',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array (),
+ 'zerofill' => false,
+ ),
+ 'bbcode_desc' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'bbcode_desc',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_tag' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 255,
+ 'name' => 'bbcode_tag',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_replace' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'bbcode_replace',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_useoption' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_useoption',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_example' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'bbcode_example',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_switch_option' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_switch_option',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_menu_option_text' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 200,
+ 'name' => 'bbcode_menu_option_text',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_menu_content_text' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 200,
+ 'name' => 'bbcode_menu_content_text',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_single_tag' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_single_tag',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_groups' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 255,
+ 'name' => 'bbcode_groups',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_sections' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 255,
+ 'name' => 'bbcode_sections',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_php_plugin' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 255,
+ 'name' => 'bbcode_php_plugin',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_no_parsing' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_no_parsing',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_protected' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_protected',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_aliases' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 255,
+ 'name' => 'bbcode_aliases',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_optional_option' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 1,
+ 'name' => 'bbcode_optional_option',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array (),
+ 'zerofill' => false,
+ ),
+ 'bbcode_image' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 255,
+ 'name' => 'bbcode_image',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_app' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 50,
+ 'name' => 'bbcode_app',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'bbcode_custom_regex' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'bbcode_custom_regex',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ ),
+ 'indexes' => array(
+ 'PRIMARY' => array(
+ 'type' => 'primary',
+ 'name' => 'PRIMARY',
+ 'length' => array( 0 => NULL ),
+ 'columns' => array( 0 => 'bbcode_id' ),
+ ),
+ )
+ ) );
+ }
+
+ if ( \IPS\Db::i()->checkForTable( 'bbcode_mediatag' ) )
+ {
+ \IPS\Db::i()->renameTable( 'bbcode_mediatag', 'convert_bbcode_mediatag' );
+ }
+ else
+ {
+ \IPS\Db::i()->createTable( array(
+ 'name' => 'convert_bbcode_mediatag',
+ 'columns' => array(
+ 'mediatag_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 10,
+ 'name' => 'mediatag_id',
+ 'type' => 'SMALLINT',
+ 'unsigned' => true,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'mediatag_name' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '',
+ 'length' => 255,
+ 'name' => 'mediatag_name',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'mediatag_match' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'mediatag_match',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'mediatag_replace' => array(
+ 'allow_null' => true,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 0,
+ 'name' => 'mediatag_replace',
+ 'type' => 'TEXT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'mediatag_position' => array(
+ 'allow_null' => false,
+ 'auto_increment' => false,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => '0',
+ 'length' => 6,
+ 'name' => 'mediatag_position',
+ 'type' => 'SMALLINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ ),
+ 'indexes' => array(
+ 'PRIMARY' => array(
+ 'type' => 'primary',
+ 'name' => 'PRIMARY',
+ 'length' => array( 0 => NULL ),
+ 'columns' => array( 0 => 'mediatag_id' ),
+ ),
+ )
+ ) );
+ }
+ return TRUE;
+ }
+
+ // You can create as many additional methods (step2, step3, etc.) as is necessary.
+ // Each step will be executed in a new HTTP request
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101008/queries.json b/applications/convert/setup/upg_101008/queries.json
new file mode 100644
index 00000000..aea52b62
--- /dev/null
+++ b/applications/convert/setup/upg_101008/queries.json
@@ -0,0 +1,22 @@
+{
+ "1": {
+ "method": "addColumn",
+ "params": [
+ "convert_apps",
+ {
+ "name": "db_port",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "3066",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101009/queries.json b/applications/convert/setup/upg_101009/queries.json
new file mode 100644
index 00000000..b9082874
--- /dev/null
+++ b/applications/convert/setup/upg_101009/queries.json
@@ -0,0 +1,56 @@
+{
+ "1": {
+ "method": "changeColumn",
+ "params": [
+ "convert_apps",
+ "db_port",
+ {
+ "name": "db_port",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "3306",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ },
+ "2": {
+ "method": "update",
+ "params": [
+ "convert_apps",
+ {
+ "db_port": 3306
+ },
+ [
+ "db_port=?",
+ 3066
+ ]
+ ]
+ },
+ "3": {
+ "method": "addColumn",
+ "params": [
+ "convert_logs",
+ {
+ "name": "log_time",
+ "type": "INT",
+ "length": 10,
+ "decimals": null,
+ "values": null,
+ "allow_null": false,
+ "default": "0",
+ "comment": "",
+ "unsigned": false,
+ "zerofill": false,
+ "auto_increment": false,
+ "binary": false
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101079/postUpgrade.php b/applications/convert/setup/upg_101079/postUpgrade.php
new file mode 100644
index 00000000..f8c0c616
--- /dev/null
+++ b/applications/convert/setup/upg_101079/postUpgrade.php
@@ -0,0 +1,62 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @since 16 May 2016
+ * @version SVN_VERSION_NUMBER
+ */
+
+/* Check for the older style redirect scripts, and if present tell the admin to remove. */
+$redirectScriptNames = array(
+ 'forumdisplay.php',
+ 'member.php',
+ 'showthread.php',
+ 'showgallery.php',
+ 'showphoto.php',
+ 'memberlist.php',
+ 'viewforum.php',
+ 'viewtopic.php',
+ 'board.php',
+ 'profile.php',
+ 'topic.php',
+ 'album.php',
+ 'attachment.php',
+ 'picture.php',
+ 'showpost.php',
+ 'showthread.php',
+ 'tags.php',
+ 'archive/index.php',
+ 'vb_gateway.php',
+ 'proxy.php'
+);
+
+$filesPresent = array();
+
+foreach( $redirectScriptNames as $filename )
+{
+ if( file_exists( \IPS\ROOT_PATH . '/' . $filename ) )
+ {
+ $filesPresent[] = $filename;
+ }
+}
+
+if( count( $filesPresent ) )
+{
+ $message = <<The following files are present in your Community Suite root directory to handle redirects for a conversion, but are no longer needed and should be removed:
+ •
+EOF;
+
+ $message .= implode( " • ", $filesPresent );
+
+ $message .= <<
+ Please also be sure that your .htaccess file is reset back to the default Community Suite .htaccess code. You can download the default .htaccess file for the Community Suite in the ACP under System -> Site Promotion -> Search Engine Optimization by clicking the link in the setting "Rewrite URLs?".
+
+EOF;
+
+ $message = \IPS\Theme::i()->getTemplate( 'global' )->block( NULL, $message );
+}
diff --git a/applications/convert/setup/upg_101088/queries.json b/applications/convert/setup/upg_101088/queries.json
new file mode 100644
index 00000000..62f9a2ae
--- /dev/null
+++ b/applications/convert/setup/upg_101088/queries.json
@@ -0,0 +1,28 @@
+{
+ "1": {
+ "method": "update",
+ "params": [
+ "convert_apps",
+ {
+ "app_key": "vbulletin"
+ },
+ [
+ "app_key=?",
+ "vbuletin_legacy"
+ ]
+ ]
+ },
+ "2": {
+ "method": "update",
+ "params": [
+ "convert_apps",
+ {
+ "app_key": "vbulletin"
+ },
+ [
+ "app_key=?",
+ "vbulletin_legacy36"
+ ]
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_101090/queries.json b/applications/convert/setup/upg_101090/queries.json
new file mode 100644
index 00000000..62f9a2ae
--- /dev/null
+++ b/applications/convert/setup/upg_101090/queries.json
@@ -0,0 +1,28 @@
+{
+ "1": {
+ "method": "update",
+ "params": [
+ "convert_apps",
+ {
+ "app_key": "vbulletin"
+ },
+ [
+ "app_key=?",
+ "vbuletin_legacy"
+ ]
+ ]
+ },
+ "2": {
+ "method": "update",
+ "params": [
+ "convert_apps",
+ {
+ "app_key": "vbulletin"
+ },
+ [
+ "app_key=?",
+ "vbulletin_legacy36"
+ ]
+ ]
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/setup/upg_33001/upgrade.php b/applications/convert/setup/upg_33001/upgrade.php
new file mode 100644
index 00000000..a87ae082
--- /dev/null
+++ b/applications/convert/setup/upg_33001/upgrade.php
@@ -0,0 +1,338 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @license http://www.invisionpower.com/legal/standards/
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 01 Feb 2017
+ * @version SVN_VERSION_NUMBER
+ */
+
+namespace IPS\convert\setup\upg_33001;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * 1.2.3 Upgrade Code
+ */
+class _Upgrade
+{
+ /**
+ * Create extra conv_link tables
+ *
+ * @return array If returns TRUE, upgrader will proceed to next step. If it returns any other value, it will set this as the value of the 'extra' GET parameter and rerun this step (useful for loops)
+ */
+ public function step1()
+ {
+ if ( !\IPS\Db::i()->checkForTable( 'conv_link_pms' ) )
+ {
+ \IPS\Db::i()->createTable( array (
+ 'name' => 'conv_link_pms',
+ 'columns' => array(
+ 'link_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 10,
+ 'name' => 'link_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'ipb_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'ipb_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'foreign_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'foreign_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'type' => array(
+ 'allow_null' => true,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 32,
+ 'name' => 'type',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'duplicate' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 1,
+ 'name' => 'duplicate',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'app' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'app',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ ),
+ 'indexes' => array(
+ 'PRIMARY' => array(
+ 'type' => 'primary',
+ 'name' => 'PRIMARY',
+ 'length' => array( 0 => NULL ),
+ 'columns' => array( 0 => 'link_id' ),
+ ),
+ )
+ ) );
+ }
+
+ if ( !\IPS\Db::i()->checkForTable( 'conv_link_topics' ) )
+ {
+ \IPS\Db::i()->createTable( array (
+ 'name' => 'conv_link_topics',
+ 'columns' => array(
+ 'link_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 10,
+ 'name' => 'link_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'ipb_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'ipb_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'foreign_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'foreign_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'type' => array(
+ 'allow_null' => true,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 32,
+ 'name' => 'type',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'duplicate' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 1,
+ 'name' => 'duplicate',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'app' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'app',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ ),
+ 'indexes' => array(
+ 'PRIMARY' => array(
+ 'type' => 'primary',
+ 'name' => 'PRIMARY',
+ 'length' => array( 0 => NULL ),
+ 'columns' => array( 0 => 'link_id' ),
+ ),
+ )
+ ) );
+ }
+
+ if ( !\IPS\Db::i()->checkForTable( 'conv_link_posts' ) )
+ {
+ \IPS\Db::i()->createTable( array (
+ 'name' => 'conv_link_posts',
+ 'columns' => array(
+ 'link_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 10,
+ 'name' => 'link_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'ipb_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'ipb_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'foreign_id' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'foreign_id',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'type' => array(
+ 'allow_null' => true,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => NULL,
+ 'length' => 32,
+ 'name' => 'type',
+ 'type' => 'VARCHAR',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'duplicate' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 1,
+ 'name' => 'duplicate',
+ 'type' => 'TINYINT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ 'app' => array(
+ 'allow_null' => false,
+ 'auto_increment' => true,
+ 'binary' => false,
+ 'comment' => '',
+ 'decimals' => NULL,
+ 'default' => 0,
+ 'length' => 10,
+ 'name' => 'app',
+ 'type' => 'INT',
+ 'unsigned' => false,
+ 'values' => array(),
+ 'zerofill' => false,
+ ),
+ ),
+ 'indexes' => array(
+ 'PRIMARY' => array(
+ 'type' => 'primary',
+ 'name' => 'PRIMARY',
+ 'length' => array( 0 => NULL ),
+ 'columns' => array( 0 => 'link_id' ),
+ ),
+ )
+ ) );
+ }
+ return TRUE;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/App.php b/applications/convert/sources/App.php
new file mode 100644
index 00000000..6972bf0b
--- /dev/null
+++ b/applications/convert/sources/App.php
@@ -0,0 +1,599 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _App extends \IPS\Patterns\ActiveRecord
+{
+ /**
+ * @brief [ActiveRecord] ID Database Column
+ */
+ public static $databaseColumnId = 'app_id';
+
+ /**
+ * @brief [ActiveRecord] Database table
+ */
+ public static $databaseTable = 'convert_apps';
+
+ /**
+ * @brief [ActiveRecord] Database ID Fields
+ */
+ protected static $databaseIdFields = array( 'name' );
+
+ /**
+ * @brief Array Storage of loaded ID links.
+ */
+ protected $linkCache = array();
+
+ /**
+ * @brief Flag to indicate the log is simply a notice, and that it is informational only. Useful for when data is missing, but can be covered via default values.
+ */
+ const LOG_NOTICE = 1;
+
+ /**
+ * @brief Flag to indicate the log is a warning, and should be checked to see if conversion happened correctly. Useful for indicating something cannot be converted due to being orphaned or otherwise having missing data (ex. no parent topic).
+ */
+ const LOG_WARNING = 2;
+
+ /**
+ * @brief Flag to indicate something went wrong, and the data did not convert correctly. Useful for indicating when something legitimately does not convert, but should.
+ */
+ const LOG_ERROR = 3;
+
+ /**
+ * Get converted apps
+ *
+ * @return array
+ */
+ public static function apps()
+ {
+ if ( !isset( \IPS\Data\Store::i()->convert_apps ) )
+ {
+ try
+ {
+ $rows = iterator_to_array( \IPS\Db::i()->select( '*', 'convert_apps', array(), 'app_id ASC' ) );
+ }
+ catch ( \IPS\Db\Exception $e )
+ {
+ if ( $e->getCode() === 1146 )
+ {
+ $rows = iterator_to_array( \IPS\Db::i()->select( '*', 'conv_apps', array(), 'app_id ASC' ) );
+ }
+ else
+ {
+ throw $e;
+ }
+ }
+
+ \IPS\Data\Store::i()->convert_apps = $rows;
+ }
+
+ return new \IPS\Patterns\ActiveRecordIterator( new \ArrayIterator( \IPS\Data\Store::i()->convert_apps ), 'IPS\convert\App' );
+
+ }
+
+ /**
+ * [ActiveRecord] Save Record
+ *
+ * @return void
+ */
+ public function save()
+ {
+ if ( !$this->app_id )
+ {
+ $this->start_date = time();
+ parent::save();
+
+ \IPS\convert\Application::checkConvParent( $this->getSource()->getLibrary()->app );
+
+ \IPS\Db::i()->insert( 'convert_app_sessions', array(
+ 'session_app_id' => $this->app_id,
+ 'session_app_data' => json_encode( array( 'completed' => array(), 'working' => array(), 'more_info' => array() ) ),
+ ) );
+ }
+
+ $classname = get_class( $this->getSource( TRUE, FALSE ) );
+ $this->login = ( $classname::loginEnabled() === TRUE ) ? 1 : 0;
+ $this->db_driver = 'mysql'; /* I was going to drop this, but it has the potential for expansion in the future, as all we do is select from the source */
+ $this->app_merge = 1;
+ parent::save();
+
+ unset( \IPS\Data\Store::i()->convert_apps );
+ }
+
+ /**
+ * [ActiveRecord] Delete Record
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ foreach( array( 'convert_link', 'convert_link_pms', 'convert_link_topics', 'convert_link_posts' ) AS $table )
+ {
+ \IPS\Db::i()->delete( $table, array( 'app=?', $this->app_id ) );
+ }
+
+ \IPS\Db::i()->delete( 'convert_app_sessions', array( 'session_app_id=?', $this->app_id ) );
+ \IPS\Db::i()->delete( 'convert_logs', array( 'log_app=?', $this->app_id ) );
+
+ parent::delete();
+
+ unset( \IPS\Data\Store::i()->convert_apps );
+ }
+
+ /**
+ * Save Session Data for this application
+ *
+ * @param array Session Data
+ * @return void
+ */
+ public function set__session( $value )
+ {
+ \IPS\Db::i()->update( 'convert_app_sessions', array( 'session_app_data' => json_encode( $value ) ), array( 'session_app_id=?', $this->app_id ) );
+ }
+
+ /**
+ * Get Session Data for this application
+ *
+ * @return array
+ */
+ public function get__session()
+ {
+ try
+ {
+ return json_decode( \IPS\Db::i()->select( 'session_app_data', 'convert_app_sessions', array( 'session_app_id=?', $this->app_id ) )->first(), TRUE );
+ }
+ catch( \Exception $e )
+ {
+ /* If it doesn't exist, create it in the database and return an empty array. */
+ \IPS\Db::i()->insert( 'convert_app_sessions', array(
+ 'session_app_id' => $this->app_id,
+ 'session_app_data' => json_encode( array( 'completed' => array(), 'working' => array(), 'more_info' => array() ) )
+ ) );
+ return array( 'completed' => array(), 'working' => array(), 'more_info' => array() );
+ }
+ }
+
+ /**
+ * [Legacy] Get Software. Automatically adjusts if legacy software which has since had its application key changed.
+ *
+ * @return string
+ */
+ public function get_sw()
+ {
+ switch( $this->_data['sw'] )
+ {
+ case 'board':
+ return 'forums';
+ break;
+
+ case 'ccs':
+ return 'cms';
+ break;
+
+ default:
+ return $this->_data['sw'];
+ break;
+ }
+ }
+
+ /**
+ * [Legacy] Automatically fix any legacy application keys that have changed.
+ *
+ * @return void
+ */
+ public function set_sw( $value )
+ {
+ switch( $value )
+ {
+ case 'board':
+ $this->_data['sw'] = 'forums';
+ break;
+
+ case 'ccs':
+ $this->_data['sw'] = 'cms';
+ break;
+
+ default:
+ $this->_data['sw'] = $value;
+ break;
+ }
+ }
+
+ /**
+ * @brief Parent Store
+ */
+ protected $parentStore = NULL;
+
+ /**
+ * Get parent application
+ *
+ * @return \IPS\convert\App
+ * @throws \BadMethodCallException
+ */
+ public function get__parent()
+ {
+ if ( is_null( $this->parentStore ) )
+ {
+ if ( ! $this->parent )
+ {
+ throw new \BadMethodCallException;
+ }
+
+ try
+ {
+ $this->parentStore = static::constructFromData( \IPS\Db::i()->select( '*', 'convert_apps', array( "app_id=?", $this->parent ) )->first() );
+ }
+ catch( \UnderflowException $e )
+ {
+ throw new \BadMethodCallException;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ throw new \BadMethodCallException;
+ }
+ }
+
+ return $this->parentStore;
+ }
+
+ /**
+ * Retrieves an IPS Community Suite ID from a Foreign ID.
+ *
+ * @param mixed $foreign_id The Foreign ID
+ * @param string|array $type The type of item, or an array of types to check.
+ * @param boolean $parent If set to TRUE, then retrieves from the parent application if one is available.
+ * @param boolean $mainTable If set to TRUE, and the type is of either 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', or 'forums_posts', then the ID is retrieved from convert_link, rather than the other link tables.
+ *
+ * @return integer The IPS Community Suite ID.
+ * @throws \OutOfRangeException
+ */
+ public function getLink( $foreign_id, $type, $parent=FALSE, $mainTable=FALSE )
+ {
+ if ( !is_array( $type ) )
+ {
+ $type = array( $type );
+ }
+
+ $key = md5( json_encode( $type ) );
+
+ if ( isset( $this->linkCache[$key][$foreign_id] ) )
+ {
+ return $this->linkCache[$key][$foreign_id];
+ }
+
+ $table = 'convert_link';
+
+ if ( $mainTable === FALSE )
+ {
+ foreach( $type AS $t )
+ {
+ if ( in_array( $t, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', 'forums_posts' ) ) )
+ {
+ if ( in_array( $t, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map' ) ) )
+ {
+ $table = 'convert_link_pms';
+ }
+ else
+ {
+ $tableType = str_replace( 'forums_', '', $t );
+ $table = "convert_link_{$tableType}";
+ }
+ }
+ }
+ }
+
+ try
+ {
+ $link = \IPS\Db::i()->select( 'ipb_id', $table, array( \IPS\Db::i()->in( 'type', $type ) . ' AND foreign_id=? AND app=?', (string) $foreign_id, ( $parent === TRUE ) ? $this->parent : $this->app_id ) )->first();
+ $this->linkCache[$key][$foreign_id] = $link;
+ return $this->linkCache[$key][$foreign_id];
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If lookup failed, and we have a parent, try it anyway */
+ try
+ {
+ $link = \IPS\Db::i()->select( 'ipb_id', $table, array( \IPS\Db::i()->in( 'type', $type ) . ' AND foreign_id=? AND app=?', (string) $foreign_id, $this->parent ) )->first();
+ $this->linkCache[$key][$foreign_id] = $link;
+ return $this->linkCache[$key][$foreign_id];
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Still here? Throw the exception */
+ throw new \OutOfRangeException( 'link_invalid' );
+ }
+ }
+
+ /**
+ * @brief Sibling Link Cache
+ */
+ protected $siblingLinkCache = array();
+
+ /**
+ * Retrieves an IPS Community Suite iD from a Foriegn ID in a Sibling Application
+ *
+ * @param mixed $foreign_id The Foreign ID.
+ * @param string $type The type of item.
+ * @param string $sibling The sibling software library.
+ * @param boolean $mainTable If set to TRUE, and the type is of either 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', or 'forums_posts', then the ID is retrieved from convert_link, rather than the other link tables.
+ *
+ * @return inteer The IPS Community Suite ID.
+ * @throws \OutOfRangeException
+ */
+ public function getSiblingLink( $foreign_id, $type, $sibling, $mainTable=FALSE )
+ {
+ if ( isset( $this->siblingLinkCache[$type][$sibling][$foreign_id] ) )
+ {
+ return $this->siblingLinkCache[$type][$sibling][$foreign_id];
+ }
+
+ try
+ {
+ $sibling = static::constructFromData( \IPS\Db::i()->select( '*', 'convert_apps', array( "sw=? AND parent=?", $sibling, $this->parent ) )->first() );
+ }
+ catch( \UnderflowException $e )
+ {
+ throw new \OutOfRangeException( 'sibling_invalid' );
+ }
+
+ return $sibling->getLink( $foreign_id, $type, FALSE, $mainTable );
+ }
+
+ /**
+ * Saves a foreign ID to IPS Community Suite ID reference to the convert_link tables.
+ *
+ * @param integer $ips_id The IPS Community Suite ID
+ * @param mixed $foreign_id The Foreign ID
+ * @param string $type The type of item
+ * @param boolean $duplicate If TRUE, then this item is a duplicate and was merged into existing $ips_id
+ * @param boolean $mainTable If TRUE, then link will be stored in the main convert_link table even if $type is 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', or 'forums_posts'
+ *
+ * @return void
+ */
+ public function addLink( $ips_id, $foreign_id, $type, $duplicate=FALSE, $mainTable=FALSE )
+ {
+ $table = 'convert_link';
+
+ if ( in_array( $type, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', 'forums_posts' ) ) AND $mainTable === FALSE )
+ {
+ if ( in_array( $type, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map' ) ) )
+ {
+ $table = 'convert_link_pms';
+ }
+ else
+ {
+ $tableType = str_replace( 'forums_', '', $type );
+ $table = "convert_link_{$tableType}";
+ }
+ }
+
+ \IPS\Db::i()->insert( $table, array(
+ 'ipb_id' => $ips_id,
+ 'foreign_id' => $foreign_id,
+ 'type' => $type,
+ 'duplicate' => ( $duplicate === TRUE ) ? 1 : 0,
+ 'app' => $this->app_id
+ ) );
+
+ $this->linkCache[$type][$foreign_id] = $ips_id;
+ }
+
+ /**
+ * Checks to see if a link exists for an IPS Community Suite ID
+ *
+ * @param integer $ips_id The IPS Community Suite ID.
+ * @return void
+ * @throws \OutOfRangeException
+ */
+ public function checkLink( $ips_id, $type, $mainTable=FALSE )
+ {
+ $table = 'convert_link';
+
+ if ( in_array( $type, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map', 'forums_topics', 'forums_posts' ) ) AND $mainTable === FALSE )
+ {
+ if ( in_array( $type, array( 'core_message_topics', 'core_message_posts', 'core_message_topic_user_map' ) ) )
+ {
+ $table = 'convert_link_pms';
+ }
+ else
+ {
+ $tableType = str_replace( 'forums_', '', $type );
+ $table = "convert_link_{$tableType}";
+ }
+ }
+
+ try
+ {
+ $link = \IPS\Db::i()->select( '*', $table, array( "app=? AND ipb_id=? AND type=?", $this->app_id, $ips_id, $type ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ throw new \OutOfRangeException;
+ }
+ }
+
+ /**
+ * Checks to see if this application also has a sibling of a specific type
+ *
+ * @param string $software The application key to look for
+ * @return void
+ * @throws \OutOfRangeException
+ */
+ public function checkForSibling( $software )
+ {
+ try
+ {
+ \IPS\Db::i()->select( '*', 'convert_apps', array( "sw=? AND parent=?", $software, $this->parent ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ throw new \OutOfRangeException;
+ }
+ }
+
+ /**
+ * @brief Sibling Cache
+ */
+ protected $siblingCache = array();
+
+ /**
+ * Construct an \IPS\convert\App object for a sibling application.
+ *
+ * @param string $software The application key to look for
+ * @return \IPS\convert\APP
+ * @throws \OutOfRangeException
+ */
+ public function getSibling( $software )
+ {
+ if ( !isset( $this->siblingCache[$software] ) )
+ {
+ try
+ {
+ $this->siblingCache[$software] = static::constructFromData( \IPS\Db::i()->select( '*', 'convert_apps', array( "sw=? AND parent=?", $software, $this->parent ) )->first() );
+ }
+ catch( \Exception $e )
+ {
+ throw new \OutOfRangeException;
+ }
+ }
+
+ return $this->siblingCache[$software];
+ }
+
+ /**
+ * Fetch the source application class file.
+ *
+ * @param bool Construct the object
+ * @param bool Establish a database connection ($construct must be TRUE)
+ * @return \IPS\convert\Software|string
+ * @throws \InvalidArgumentException
+ */
+ public function getSource( $construct=TRUE, $needDB=TRUE )
+ {
+ /* Update software keys for communities converted to 3.x */
+ switch( $this->_data['sw'] )
+ {
+ case 'board':
+ case 'ccs':
+ $this->sw = $this->_data['sw'];
+ $this->save();
+ break;
+ }
+
+ if( $this->_data['app_key'] == 'photopost8' )
+ {
+ $this->app_key = 'photopost';
+ $this->save();
+ }
+
+ if ( ! class_exists( 'IPS\\convert\\Software\\' . ucwords( $this->_data['sw'] ) . '\\' . ucwords( $this->_data['app_key'] ) ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_source' );
+ }
+
+ $classname = 'IPS\\convert\\Software\\' . ucwords( $this->_data['sw'] ) . '\\' . ucwords( $this->_data['app_key'] );
+
+ if ( $construct )
+ {
+ return new $classname( $this, $needDB );
+ }
+ else
+ {
+ return $classname;
+ }
+ }
+
+ /**
+ * Log Something
+ *
+ * @param string $message The message to log.
+ * @param string $method The current conversion method (convert_posts, convert_topics, etc.)
+ * @param integer $severity The severity level of the log. Default to LOG_NOTICE
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function log( $message, $method, $severity=1, $id=NULL )
+ {
+ if ( ! in_array( $severity, array( static::LOG_NOTICE, static::LOG_WARNING, static::LOG_ERROR ) ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_severity' );
+ }
+
+ \IPS\Db::i()->insert( 'convert_logs', array(
+ 'log_message' => $message,
+ 'log_app' => $this->app_id,
+ 'log_severity' => $severity,
+ 'log_method' => $method,
+ 'log_item_id' => $id,
+ 'log_time' => time()
+ ) );
+ }
+
+ /**
+ * Callback function to return all dependencies not yet converted.
+ *
+ * @param string Value from depency array
+ * @return boolean
+ */
+ public function dependencies( $value )
+ {
+ if ( !in_array( $value, $this->_session['completed'] ) )
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * Saves information about the current step to the session.
+ *
+ * @param array $values Values from the form.
+ * @return void
+ */
+ public function saveMoreInfo( $method, $values=array() )
+ {
+ $sessionData = $this->_session;
+
+ unset( $values['reconfigure'], $values['empty_local_data'] );
+
+ $this->_session = array( 'working' => $sessionData['working'], 'completed' => $sessionData['completed'], 'more_info' => array_merge( $sessionData['more_info'], array( $method => $values ) ) );
+ }
+
+ /**
+ * Magic __isset() method
+ *
+ * @param mixed $key key
+ * @return bool
+ */
+ public function __isset( $key )
+ {
+ if ( method_exists( $this, 'get_' . $key ) )
+ {
+ return TRUE;
+ }
+
+ if ( isset( $this->data[$key] ) )
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Exception.php b/applications/convert/sources/Exception.php
new file mode 100644
index 00000000..3d309ada
--- /dev/null
+++ b/applications/convert/sources/Exception.php
@@ -0,0 +1,22 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Exception extends \ErrorException
+{}
\ No newline at end of file
diff --git a/applications/convert/sources/Library.php b/applications/convert/sources/Library.php
new file mode 100644
index 00000000..5ec9aa30
--- /dev/null
+++ b/applications/convert/sources/Library.php
@@ -0,0 +1,482 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+abstract class _Library
+{
+ /**
+ * @brief Flag to indicate that we are using a specific key, and should do WHERE static::$currentKeyName > static::$currentKeyValue rather than LIMIT 0,1000
+ */
+ public static $usingKeys = FALSE;
+
+ /**
+ * @brief The name of the current key in the database for this step
+ */
+ public static $currentKeyName = NULL;
+
+ /**
+ * @brief The current value of the key
+ */
+ public static $currentKeyValue = 0;
+
+ /**
+ * @brief Amount of data being processed per cycle.
+ */
+ public static $perCycle = 2000;
+
+ /**
+ * @brief If not using keys, the current start value for LIMIT clause.
+ */
+ public static $startValue = 0;
+
+ /**
+ * @brief The current conversion step
+ */
+ public static $action = NULL;
+
+ /**
+ * @brief \IPS\convert\Software instance for the software we are converting from
+ */
+ public $software = NULL;
+
+ /**
+ * @brief Array of field types
+ */
+ protected static $fieldTypes = array( 'Address', 'Checkbox', 'CheckboxSet', 'Codemirror', 'Color', 'Date', 'Editor', 'Email', 'Member', 'Number', 'Password', 'Poll', 'Radio', 'Rating', 'Select', 'Tel', 'Text', 'TextArea', 'Upload', 'Url', 'YesNo' );
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\Software $software Software Instance we are converting from
+ * @return void
+ */
+ public function __construct( \IPS\convert\Software $software )
+ {
+ $this->software = $software;
+ }
+
+ /**
+ * When called at the start of a conversion step, indicates that we are using a specific key for WHERE clauses which nets performance improvements
+ *
+ * @param string $key The key to use
+ * @return void
+ */
+ public static function setKey( $key )
+ {
+ static::$usingKeys = TRUE;
+ static::$currentKeyName = $key;
+ }
+
+ /**
+ * When using a key reference, sets the current value of that key for the WHERE clause
+ *
+ * @param mixed $value The current value
+ * @return void
+ */
+ public function setLastKeyValue( $value )
+ {
+ $_SESSION['currentKeyValue'] = $value;
+ static::$currentKeyValue = $value;
+ }
+
+ /**
+ * Processes a conversion cycle.
+ *
+ * @param integer $data Data from the previous step.
+ * @param \IPS\convert\App $app Application Class for the current conversion
+ * @return array|NULL Data for the MultipleRedirect
+ */
+ public function process( $data, $method, $perCycle=NULL )
+ {
+ if ( !is_null( $perCycle ) )
+ {
+ static::$perCycle = $perCycle;
+ }
+
+ /* temp */
+ $classname = get_class( $this->software );
+ $canConvert = $classname::canConvert();
+
+ if( $canConvert === NULL )
+ {
+ return NULL;
+ }
+
+ static::$action = $canConvert[ $method ]['table'];
+ static::$startValue = $data;
+
+ if ( !isset( $_SESSION['convertCountRows'] ) )
+ {
+ $_SESSION['convertCountRows'] = array();
+ }
+
+ if ( !isset( $_SESSION['convertCountRows'][ $method ] ) )
+ {
+ $_SESSION['convertCountRows'][ $method ] = $this->software->countRows( static::$action, $canConvert[ $method ]['where'], true );
+ }
+
+ $total = $_SESSION['convertCountRows'][ $method ];
+
+ if ( $data >= $total )
+ {
+ $completed = $this->software->app->_session['completed'];
+ $more_info = $this->software->app->_session['more_info'];
+ if ( !in_array( $method, $completed ) )
+ {
+ $completed[] = $method;
+ }
+ $this->software->app->_session = array( 'working' => array(), 'more_info' => $more_info, 'completed' => $completed );
+ unset( $_SESSION['currentKeyValue'] );
+ unset( $_SESSION['convertCountRows'][ $method ] );
+ unset( $_SESSION['convertContinue'] );
+ return NULL;
+ }
+ else
+ {
+ /* Are we continuing? Let's figure out where we are... unfortunately, this only works when we are using keys (which is 99% of the time) */
+ if ( isset( $_SESSION['convertContinue'] ) AND $_SESSION['convertContinue'] === TRUE )
+ {
+ $table = 'convert_link';
+
+ switch( $method )
+ {
+ case 'convertForumsPosts':
+ $table = 'convert_link_posts';
+ break;
+
+ case 'convertForumsTopics':
+ $table = 'convert_link_topics';
+ break;
+
+ case 'convertPrivateMessages':
+ $table = 'convert_link_pms';
+ break;
+ }
+
+ /* Select our max foreign ID */
+ $type = $this->getMethodFromMenuRows( $method )['link_type'];
+
+ /* @todo If we ever support converting more than one CMS database, we'll need to change this */
+ if( is_array( $type ) )
+ {
+ $type = $type[0];
+ }
+
+ $lastId = \IPS\Db::i()->select( 'MAX(CAST( foreign_id AS UNSIGNED) )', $table, array( "app=? AND type=?", $this->software->app->app_id, $type ) )->first();
+
+ /* Set the data count for MR */
+ $data = \IPS\Db::i()->select( 'COUNT(*)', $table, array( "app=? AND type=?", $this->software->app->app_id, $type ) )->first();
+
+ /* Set as last key value */
+ $this->setLastKeyValue( $lastId );
+
+ /* Set this to FALSE so we don't do this again. */
+ $_SESSION['convertContinue'] = FALSE;
+ }
+
+ /* Fetch data from the software */
+ try
+ {
+ $this->software->$method();
+ }
+ catch( \IPS\convert\Software\Exception $e )
+ {
+ /* A Software Exception indicates we are done */
+ $completed = $this->software->app->_session['completed'];
+ $more_info = $this->software->app->_session['more_info'];
+ if ( !in_array( $method, $completed ) )
+ {
+ $completed[] = $method;
+ }
+ $this->software->app->_session = array( 'working' => array(), 'more_info' => $more_info, 'completed' => $completed );
+ unset( $_SESSION['currentKeyValue'] );
+ unset( $_SESSION['convertContinue'] );
+ return NULL;
+ }
+ catch( \Exception $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING );
+ throw new \IPS\convert\Exception;
+ }
+ catch( \ErrorException $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING );
+ throw new \IPS\convert\Exception;
+ }
+
+ $this->software->app->_session = array_merge( $this->software->app->_session, array( 'working' => array( $method => $data + static::$perCycle ) ) );
+ return array( $data + static::$perCycle, sprintf( \IPS\Member::loggedIn()->language()->get( 'converted_x_of_x' ), ( $data + static::$perCycle > $total ) ? $total : $data + static::$perCycle, \IPS\Member::loggedIn()->language()->addToStack( $this->getMethodFromMenuRows( $method )['step_title'] ), $total ), 100 / $total * ( $data + static::$perCycle ) );
+ }
+ }
+
+ /**
+ * Empty Conversion Data
+ *
+ * @param integer $data Data from the previous step.
+ * @param \IPS\convert\App $app Application Class for the current conversion
+ * @return array|NULL Data for the MultipleRedirect
+ */
+ public function emptyData( $data, $method )
+ {
+ $perCycle = 500;
+
+ /* temp */
+ $classname = get_class( $this->software );
+ $canConvert = $this->menuRows();
+
+ if ( !isset( $canConvert[ $method ]['link_type'] ) )
+ {
+ return NULL;
+ }
+
+ $type = $canConvert[ $method ]['link_type'];
+
+ if ( !isset( $_SESSION['emptyConvertedDataCount'] ) )
+ {
+ $count = 0;
+ /* Just one type? */
+ if ( !is_array( $type ) )
+ {
+ foreach( array( 'convert_link', 'convert_link_pms', 'convert_link_posts', 'convert_link_topics' ) as $table )
+ {
+ $count += \IPS\Db::i()->select( 'COUNT(*)', $table, array( "type=? AND app=?", $type, $this->software->app->app_id ) )->first();
+ }
+ }
+ else
+ {
+ foreach( $type as $t )
+ {
+ foreach( array( 'convert_link', 'convert_link_pms', 'convert_link_posts', 'convert_link_topics' ) as $table )
+ {
+ $count += \IPS\Db::i()->select( 'COUNT(*)', $table, array( "type=? AND app=?", $t, $this->software->app->app_id ) )->first();
+ }
+ }
+ }
+
+ $_SESSION['emptyConvertedDataCount'] = $count;
+ }
+
+ if ( $data >= $_SESSION['emptyConvertedDataCount'] )
+ {
+ unset( $_SESSION['emptyConvertedDataCount'] );
+ return NULL;
+ }
+ else
+ {
+ /* Fetch data from the software */
+ try
+ {
+ /* If we're dealing with more than one type, then we can just delete from any one at random until we're done */
+ if ( is_array( $type ) )
+ {
+ $type = array_rand( $type );
+ }
+
+ switch( $type )
+ {
+ case 'forums_topics':
+ $table = 'convert_link_topics';
+ break;
+
+ case 'forums_posts':
+ $table = 'convert_link_posts';
+ break;
+
+ case 'core_message_topics':
+ case 'core_message_posts':
+ case 'core_message_topic_user_map':
+ $table = 'convert_link_pms';
+ break;
+
+ default:
+ $table = 'convert_link';
+ break;
+ }
+
+ $total = (int) \IPS\Db::i()->select( 'COUNT(*)', $table, array( "type=? AND app=?", $type, $this->software->app->app_id ) )->first();
+ $rows = iterator_to_array( \IPS\Db::i()->select( 'link_id, ipb_id', $table, array( "type=? AND app=?", $type, $this->software->app->app_id ), "link_id ASC", array( 0, $perCycle ) )->setKeyField( 'link_id' )->setValueField( 'ipb_id' ) );
+ $def = \IPS\Db::i()->getTableDefinition( $type );
+
+ if ( isset( $def['indexes']['PRIMARY']['columns'] ) )
+ {
+ $id = array_pop( $def['indexes']['PRIMARY']['columns'] );
+ }
+
+ \IPS\Db::i()->delete( $type, array( \IPS\Db::i()->in( $id, array_values( $rows ) ) ) );
+ \IPS\Db::i()->delete( $table, array( \IPS\Db::i()->in( 'link_id', array_keys( $rows ) ) ) );
+ }
+ catch( \Exception $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING );
+ throw new \IPS\convert\Exception;
+ }
+ catch( \ErrorException $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_ERROR );
+ throw new \IPS\convert\Exception;
+ }
+
+ return array( $data + $perCycle, sprintf( \IPS\Member::loggedIn()->language()->get( 'removed_x_of_x' ), ( $data + $perCycle > $total ) ? $_SESSION['emptyConvertedDataCount'] : $data + $perCycle, \IPS\Member::loggedIn()->language()->addToStack( $method ), $_SESSION['emptyConvertedDataCount'] ), 100 / $_SESSION['emptyConvertedDataCount'] * ( $data + $perCycle ) );
+ }
+ }
+
+ /**
+ * Truncates data from local database
+ *
+ * @param string Convert method to run truncate call for.
+ * @return void
+ */
+ public function emptyLocalData( $method )
+ {
+ $truncate = $this->truncate( $method );
+ foreach( $truncate as $table => $where )
+ {
+ /* Kind of a hacky way to make sure we truncate the right forums archive table */
+ if ( $table === 'forums_archive_posts' )
+ {
+ \IPS\forums\Topic\ArchivedPost::db()->delete( $table, $where );
+ }
+ else
+ {
+ \IPS\Db::i()->delete( $table, $where );
+ }
+
+ /* Remove links... we don't really care about which table they are in right now */
+ foreach( array( 'convert_link', 'convert_link_pms', 'convert_link_posts', 'convert_link_topics' ) as $linkTable )
+ {
+ \IPS\Db::i()->delete( $linkTable, array( "type=? AND app=?", $table, $this->software->app->app_id ) );
+ }
+ }
+ unset( $_SESSION['currentKeyValue'] );
+ }
+
+ /**
+ * @brief Cached convertable items
+ */
+ protected $convertable = NULL;
+
+ /**
+ * Return the items we can convert. Removes things that are empty.
+ *
+ * @return array
+ */
+ public function getConvertableItems()
+ {
+ if( $this->convertable !== NULL )
+ {
+ return $this->convertable;
+ }
+
+ $classname = get_class( $this->software );
+ $this->convertable = $classname::canConvert();
+
+ if( $this->convertable === NULL )
+ {
+ $this->convertable = array();
+ }
+
+ foreach( $this->convertable as $k => $v )
+ {
+ if( $this->software->countRows( $v['table'], $v['where'] ) == 0 )
+ {
+ unset( $this->convertable[ $k ] );
+ }
+ }
+
+ return $this->convertable;
+ }
+
+ /**
+ * Magic __call() method
+ *
+ * @param string $name The method to call without convert prefix.
+ * @param mixed $arguements Arguments to pass to the method
+ * @return mixed
+ */
+ public function __call( $name, $arguments )
+ {
+ if ( method_exists( $this, 'convert' . $name ) )
+ {
+ return call_user_func( array( $this, 'convert' . $name ), $arguments );
+ }
+ elseif ( method_exists( $this, $name ) )
+ {
+ return call_user_func( array( $this, $name ), $arguments );
+ }
+ else
+ {
+ \IPS\Log::log( "Call to undefined method in " . get_class( $this ) . "::{$name}", 'converters' );
+ return NULL;
+ }
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to complete this conversion
+ *
+ * @return string
+ */
+ public function getPostConversionInformation()
+ {
+ $appId = $this->software->app->parent ?: $this->software->app->app_id;
+ return \IPS\Member::loggedIn()->language()->addToStack( 'conversion_info_message', FALSE, array( 'sprintf' => array(
+ (string) \IPS\Http\Url::internal( "app=convert&module=manage&controller=create&_new=1" ),
+ (string) \IPS\Http\Url::internal( "app=convert&module=manage&controller=convert&do=finish&id={$appId}" )
+ ) ) );
+ }
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ abstract public function menuRows();
+
+ /**
+ * Get method from menu rows - abstracted to allow 'fake' entries not in menuRows()
+ *
+ * @param string $method Method requested
+ * @return array
+ */
+ public function getMethodFromMenuRows( $method )
+ {
+ $menuRows = $this->menuRows();
+ if( isset( $menuRows[ $method ] ) )
+ {
+ return $menuRows[ $method ];
+ }
+
+ return array();
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @param string The method to truncate
+ * @return array
+ */
+ abstract protected function truncate( $method );
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Blog.php b/applications/convert/sources/Library/Blog.php
new file mode 100644
index 00000000..a6f87168
--- /dev/null
+++ b/applications/convert/sources/Library/Blog.php
@@ -0,0 +1,923 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * @note We must extend the Core Library here so we can access methods like convertAttachment, convertFollow, etc
+ */
+class _Blog extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'blog';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertBlogs':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_blogs',
+ 'step_method' => 'convertBlogs',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'blog_blogs' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array(),
+ 'link_type' => 'blog_blogs',
+ );
+ break;
+
+ case 'convertBlogEntries':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_blog_entries',
+ 'step_method' => 'convertBlogEntries',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'blog_entries' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertBlogs' ),
+ 'link_type' => 'blog_entries',
+ );
+ break;
+
+ case 'convertBlogComments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_blog_comments',
+ 'step_method' => 'convertBlogComments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'blog_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertBlogEntries' ),
+ 'link_type' => 'bog_comments',
+ );
+ break;
+
+ case 'convertBlogRssImport':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_blog_rss_import',
+ 'step_method' => 'convertBlogRssImport',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'blog_rss_import' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertBlogs' ),
+ 'link_type' => 'blog_rss_import',
+ );
+ break;
+
+ case 'convertAttachments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_blog_attachments',
+ 'step_method' => 'convertAttachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( 'location_key=? AND id1<>? AND id2 IS NULL', 'blog_Entries', 0 ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 250,
+ 'dependencies' => array( 'convertBlogEntries', 'convertBlogComments' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ /* Should return multiple array members for each table that needs to be truncated. The key should be the table, while the value should be a WHERE clause, or NULL to completely empty the table */
+ case 'convertBlogs':
+ $return['convertBlogs'] = array( 'blog_blogs' => NULL );
+ break;
+
+ case 'convertBlogEntries':
+ $return['convertBlogEntries'] = array( 'blog_entries' => NULL );
+ break;
+
+ case 'convertBlogComments':
+ $return['convertBlogComments'] = array( 'blog_comments' => NULL );
+ break;
+
+ case 'convertBlogRssImport':
+ $return['convertBlogRssImport'] = array( 'blog_rss_import' => NULL, 'blog_rss_imported' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'core_attachments' => \IPS\Db::i()->in( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( "location_key=?", 'blog_Entries' ) ) ),
+ 'core_attachments_map' => array( "location_key=?", 'blog_Entries' )
+ );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a Blog
+ *
+ * @param array $info Array of blog information
+ * @param array|NULL $socialgroup Array of data to be stored in core_sys_social_groups and core_sys_social_group_members
+ * @param string|NULL|boolean $filepath URL/Path to cover photo, NULL to use raw file data, or FALSE to not convert
+ * @param string|NULL $filedata If $filepath is NULL, this should contain the raw contents of the cover photo
+ * @return integer|boolean The ID of the inserted blog, or FALSE on failure.
+ */
+ public function convertBlog( array $info, $socialgroup=NULL, $filepath=FALSE, $filedata=NULL )
+ {
+ if ( !isset( $info['blog_id'] ) )
+ {
+ $this->software->app->log( 'blog_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['blog_member_id'] ) AND $info['blog_member_id'] )
+ {
+ try
+ {
+ $info['blog_member_id'] = $this->software->app->getLink( $info['blog_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['blog_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['blog_member_id'] = 0;
+ }
+
+ if ( !isset( $info['blog_num_views'] ) )
+ {
+ $info['blog_num_views'] = 0;
+ }
+
+ if ( !isset( $info['blog_pinned'] ) )
+ {
+ $info['blog_pinned'] = 0;
+ }
+
+ if ( !isset( $info['blog_disabled'] ) )
+ {
+ $info['blog_disabled'] = 0;
+ }
+
+ if ( !isset( $info['blog_allowguests'] ) )
+ {
+ $info['blog_allowguests'] = 1;
+ }
+
+ if ( !isset( $info['blog_rating_total'] ) )
+ {
+ $info['blog_rating_total'] = 0;
+ }
+
+ if ( !isset( $info['blog_rating_count'] ) )
+ {
+ $info['blog_rating_count'] = 0;
+ }
+
+ if ( isset( $info['blog_settings'] ) )
+ {
+ if ( !is_array( $info['blog_settings'] ) )
+ {
+ $info['blog_settings'] = json_decode( $info['blog_settings'], TRUE );
+ }
+
+ if ( !is_null( $info['blog_settings'] ) )
+ {
+ $newSettings = array();
+
+ /* This looks funny, but allow for easy further expansion later */
+ foreach( array( 'allowrss' ) AS $key )
+ {
+ switch( $key )
+ {
+ case 'allowrss':
+ if ( !isset( $info['blog_settings'][ $key ] ) )
+ {
+ $newSettings[ $key ] = true;
+ }
+ else
+ {
+ $newSettings[ $key ] = (bool) $info['blog_settings'][ $key ];
+ }
+ break;
+ }
+ }
+
+ $info['blog_settings'] = json_encode( $newSettings );
+ }
+ else
+ {
+ $info['blog_settings'] = json_encode( array( 'allowrss' => TRUE ) );
+ }
+ }
+ else
+ {
+ $info['blog_settings'] = json_encode( array( 'allowrss' => TRUE ) );
+ }
+
+ /* No Longer Used */
+ $info['blog_last_visitors'] = NULL;
+ $info['blog_editors'] = NULL;
+
+ /* Name and Description */
+ if ( !isset( $info['blog_name'] ) )
+ {
+ $owner = \IPS\Member::load( $info['blog_member_id'] );
+
+ if ( $owner->member_id )
+ {
+ $name = "{$member->name}'s Blog";
+ }
+ else
+ {
+ $name = "Untitled Blog {$info['blog_id']}";
+ }
+ }
+ else
+ {
+ $name = $info['blog_name'];
+ unset( $info['blog_name'] );
+ }
+
+ $info['blog_seo_name'] = \IPS\Http\Url::seoTitle( $name );
+
+ if ( isset( $info['blog_description'] ) )
+ {
+ $desc = $info['blog_description'];
+ unset( $info['blog_description'] );
+ }
+ else
+ {
+ $desc = '';
+ }
+
+ if ( isset( $info['blog_groupblog_ids'] ) )
+ {
+ if ( !is_array( $info['blog_groupblog_ids'] ) )
+ {
+ $info['blog_groupblog_ids'] = explode( ',', $info['blog_groupblog_ids'] );
+ }
+
+ $newGroups = array();
+ if ( count( $info['blog_groupblog_ids'] ) )
+ {
+ foreach( $info['blog_groupblog_ids'] AS $group )
+ {
+ try
+ {
+ $newGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+ }
+
+ if ( count( $newGroups ) )
+ {
+ $info['blog_groupblog_ids'] = implode( ',', $newGroups );
+ }
+ else
+ {
+ $info['blog_groupblog_ids'] = '';
+ }
+ }
+ else
+ {
+ $info['blog_groupblog_ids'] = '';
+ }
+
+ if ( isset( $info['blog_last_edate'] ) )
+ {
+ if ( $info['blog_last_edate'] instanceof \IPS\DateTime )
+ {
+ $info['blog_last_edate'] = $info['blog_last_edate']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['blog_last_edate'] = 0;
+ }
+
+ /* Counts */
+ foreach( array( 'blog_count_entries', 'blog_count_comments', 'blog_count_entries_hidden', 'blog_count_comments_hidden', 'blog_rating_average', 'blog_count_entries_future' ) AS $column )
+ {
+ if ( !isset( $info[ $column ] ) )
+ {
+ $info[ $column ] = 0;
+ }
+ }
+
+ /* Well update this later if we need too */
+ $info['blog_social_group'] = NULL;
+
+ if ( !is_null( $socialgroup ) AND is_array( $socialgroup ) )
+ {
+ $socialGroupId = \IPS\Db::i()->insert( 'core_sys_social_groups', array( 'owner_id' => $info['blog_member_id'] ) );
+ $members = array();
+ $members[] = array( 'group_id' => $socialGroupId, 'member_id' => $info['blog_member_id'] );
+ foreach( $socialgroup['members'] AS $member )
+ {
+ try
+ {
+ $members[] = array( 'group_id' => $socialGroupId, 'member_id' => $this->software->app->getLink( $member, 'core_members', TRUE ) );
+ }
+ catch( \OutOfRangeExceptin $e )
+ {
+ continue;
+ }
+ }
+ \IPS\Db::i()->insert( 'core_sys_social_group_members', $members );
+
+ $info['blog_social_group'] = $socialGroupId;
+ }
+
+ /* And now cover photo */
+ if ( isset( $info['blog_cover_photo'] ) AND ( !is_null( $filedata ) OR !is_null( $filepath ) ) )
+ {
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = file_get_contents( $filepath );
+ }
+
+ $file = \IPS\File::create( 'blog_Blogs', $info['blog_cover_photo'], $filedata );
+ $info['blog_cover_photo'] = (string) $file;
+ $info['blog_cover_photo_offset'] = 0;
+ }
+ catch( \Exception $e )
+ {
+ $info['blog_cover_photo'] = NULL;
+ $info['blog_cover_photo_offset'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['blog_cover_photo'] = NULL;
+ $info['blog_cover_photo_offset'] = NULL;
+ }
+ }
+ else
+ {
+ $info['blog_cover_photo'] = NULL;
+ $info['blog_cover_photo_offset'] = NULL;
+ }
+
+ $id = $info['blog_id'];
+ unset( $info['blog_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'blog_blogs', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'blog_blogs' );
+
+ \IPS\Lang::saveCustom( 'blog', "blogs_blog_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'blog', "blogs_blog_{$inserted_id}_desc", $desc );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a blog entry
+ *
+ * @param array $info Array of entry information
+ * @param string|NULL $filepath Path to the Cover Photo, or NULL
+ * @param string|NULL $filedata Binary data for cover photo, or NULL
+ * @return integer|boolean The ID of the inserted entry, or FALSE on failure
+ */
+ public function convertBlogEntry( array $info, $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['entry_id'] ) )
+ {
+ $this->software->app->log( 'blog_entry_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['entry_blog_id'] ) )
+ {
+ try
+ {
+ $info['entry_blog_id'] = $this->software->app->getLink( $info['entry_blog_id'], 'blog_blogs' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'blog_entry_missing_blog', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['entry_id'] );
+ return FALSE;
+ }
+ }
+
+ if ( !isset( $info['entry_name'] ) )
+ {
+ $info['entry_name'] = "Untitled Blog Entry {$info['entry_id']}";
+ $this->software->app->log( 'blog_entry_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['entry_id'] );
+ }
+
+ $info['entry_name_seo'] = \IPS\Http\Url::seoTitle( $info['entry_name'] );
+
+ if ( empty( $info['entry_content'] ) )
+ {
+ $this->software->app->log( 'blog_entry_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['entry_content'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['entry_author_id'] ) )
+ {
+ try
+ {
+ $info['entry_author_id'] = $this->software->app->getLink( $info['entry_author_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['entry_author_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['entry_author_id'] = 0;
+ }
+
+ if ( !isset( $info['entry_author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['entry_author_id'] );
+
+ if ( $author->member_id )
+ {
+ $info['entry_author_name'] = $author->name;
+ }
+ else
+ {
+ $info['entry_author_name'] = "Guest";
+ }
+ }
+
+ if ( isset( $info['entry_date'] ) )
+ {
+ if ( $info['entry_date'] instanceof \IPS\DateTime )
+ {
+ $info['entry_date'] = $info['entry_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['entry_date'] = time();
+ }
+
+ if ( !isset( $info['entry_status'] ) OR !in_array( $info['entry_status'], array( 'published', 'draft' ) ) )
+ {
+ $info['entry_status'] = 'published';
+ }
+
+ /* Zero Defaults */
+ foreach( array( 'entry_locked', 'entry_num_comments', 'entry_queued_comments', 'entry_featured', 'entry_views', 'entry_pinned', 'entry_hidden_comments', 'entry_is_future_entry' ) AS $zeroDefault )
+ {
+ if ( !isset( $info[ $zeroDefault ] ) )
+ {
+ $info[ $zeroDefault ] = 0;
+ }
+ }
+
+ if ( isset( $info['entry_last_comment_mid'] ) )
+ {
+ try
+ {
+ $info['entry_last_comment_mid'] = $this->software->app->getLink( $info['entry_last_comment_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['entry_last_comment_mid'] = 0;
+ }
+ }
+ else
+ {
+ $info['entry_last_comment_mid'] = 0;
+ }
+
+ if ( !isset( $info['entry_hidden'] ) )
+ {
+ $info['entry_hidden'] = 1; # backwards
+ }
+
+ $info['entry_post_key'] = NULL;
+
+ if ( isset( $info['entry_edit_time'] ) )
+ {
+ if ( $info['entry_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['entry_edit_time'] = $info['entry_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['entry_edit_time'] = NULL;
+ }
+
+ if ( !isset( $info['entry_edit_name'] ) )
+ {
+ $info['entry_edit_name'] = NULL;
+ }
+
+ if ( isset( $info['entry_last_update'] ) )
+ {
+ if ( $info['entry_last_update'] instanceof \IPS\DateTime )
+ {
+ $info['entry_last_update'] = $info['entry_last_update']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['entry_last_update'] = $info['entry_date'];
+ }
+
+ if ( isset( $info['entry_gallery_album'] ) )
+ {
+ try
+ {
+ $info['entry_gallery_album'] = $this->software->app->getSiblingLink( $info['entry_gallery_album'], 'gallery_albums', 'gallery' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['entry_gallery_album'] = NULL;
+ }
+ }
+ else
+ {
+ $info['entry_gallery_album'] = NULL;
+ }
+
+ if ( isset( $info['entry_poll_state'] ) AND is_array( $info['entry_poll_state'] ) )
+ {
+ if ( $poll = $this->convertPoll( $info['entry_poll_state']['poll_data'], $info['entry_poll_state']['vote_data'] ) )
+ {
+ $info['entry_poll_state'] = $poll;
+ }
+ else
+ {
+ $info['entry_poll_state'] = NULL;
+ }
+ }
+ else
+ {
+ $info['entry_poll_state'] = NULL;
+ }
+
+ if ( isset( $info['entry_publish_date'] ) )
+ {
+ if ( $info['entry_publish_date'] instanceof \IPS\DateTime )
+ {
+ $info['entry_publish_date'] = $info['entry_publish_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['entry_publish_date'] = 0;
+ }
+
+ $info['entry_image'] = '';
+
+ if ( !isset( $info['entry_ip_address'] ) OR filter_var( $info['entry_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['entry_ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['entry_cover_photo'] ) AND ( !is_null( $filepath ) OR !is_null( $filedata ) ) )
+ {
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = file_get_contents( $filepath );
+ }
+
+ $file = \IPS\File::create( 'blog_Entries', $info['entry_cover_photo'], $filedata );
+ $info['entry_cover_photo'] = (string) $file;
+ $info['entry_cover_photo_offset'] = 0;
+ }
+ catch( \Exception $e )
+ {
+ $info['entry_cover_photo'] = NULL;
+ $info['entry_cover_photo_offset'] = NULL;
+ }
+ }
+ else
+ {
+ $info['entry_cover_photo'] = NULL;
+ $info['entry_cover_photo_offset'] = NULL;
+ }
+
+ $id = $info['entry_id'];
+ unset( $info['entry_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'blog_entries', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'blog_entries' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a blog comment
+ *
+ * @param array $info Array of comment information
+ * @return integer|boolean The ID of the inserted comment, or FALSE on failure.
+ */
+ public function convertBlogComment( array $info )
+ {
+ if ( !isset( $info['comment_id'] ) )
+ {
+ $this->software->app->log( 'blog_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_entry_id'] ) )
+ {
+ try
+ {
+ $info['comment_entry_id'] = $this->software->app->getLink( $info['comment_entry_id'], 'blog_entries' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'blog_comment_missing_entry', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'blog_comment_missing_entry', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['comment_text'] ) )
+ {
+ $this->software->app->log( 'blog_comment_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_member_id'] ) )
+ {
+ try
+ {
+ $info['comment_member_id'] = $this->software->app->getLink( $info['comment_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_member_id'] = 0;
+ }
+
+ if ( !isset( $info['comment_member_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['comment_member_id'] );
+
+ if ( $author->member_id )
+ {
+ $info['comment_member_name'] = $author->name;
+ }
+ else
+ {
+ $info['comment_member_name'] = "Guest";
+ }
+ }
+
+ if ( !isset( $info['comment_ip_address'] ) OR filter_var( $info['comment_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['comment_ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['comment_date'] ) )
+ {
+ if ( $info['comment_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_date'] = $info['comment_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_date'] = time();
+ }
+
+ if ( isset( $info['comment_edit_time'] ) )
+ {
+ if ( $info['comment_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['comment_edit_time'] = $info['comment_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_edit_time'] = NULL;
+ }
+
+ if ( !isset( $info['comment_approved'] ) )
+ {
+ $info['comment_approved'] = 1;
+ }
+
+ if ( !isset( $info['comment_edit_member_name'] ) )
+ {
+ $info['comment_edit_member_name'] = NULL;
+ }
+
+ if ( !isset( $info['comment_edit_show'] ) )
+ {
+ $info['comment_edit_show'] = 0;
+ }
+
+ $id = $info['comment_id'];
+ unset( $info['comment_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'blog_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'blog_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a blog RSS import
+ *
+ * @param array $info Array of RSS Import information
+ * @return integer|boolean The ID of the inserted RSS import, or FALSE on failure
+ */
+ public function convertBlogRssImport( array $info )
+ {
+ if ( !isset( $info['rss_id'] ) )
+ {
+ $this->software->app->log( 'blog_rss_import_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['rss_blog_id'] ) )
+ {
+ try
+ {
+ $info['rss_blog_id'] = $this->software->app->getLink( $info['rss_blog_id'], 'blog_blogs' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'blog_rss_import_missing_blog', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'blog_rss_import_missing_blog', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rss_url'] ) OR filter_var( $info['rss_url'], FILTER_VALIDATE_URL ) === FALSE )
+ {
+ $this->software->app->log( 'blog_rss_import_missing_url', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rss_auth_user'] ) OR !isset( $info['rss_auth_pass'] ) )
+ {
+ $info['rss_auth_user'] = '';
+ $info['rss_auth_pass'] = '';
+ }
+
+ if ( isset( $info['rss_last_import'] ) )
+ {
+ if ( $info['rss_last_import'] instanceof \IPS\DateTime )
+ {
+ $info['rss_last_import'] = $info['rss_last_import']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['rss_last_import'] = time();
+ }
+
+ if ( isset( $info['rss_tags'] ) )
+ {
+ if ( is_array( $info['rss_tags'] ) )
+ {
+ $info['rss_tags'] = json_encode( $info['rss_tags'] );
+ }
+ }
+ else
+ {
+ $info['rss_tags'] = NULL;
+ }
+
+ if ( isset( $info['rss_member'] ) )
+ {
+ try
+ {
+ $info['rss_member'] = $this->software->app->getLink( $info['rss_member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['rss_member'] = NULL;
+ }
+ }
+ else
+ {
+ $info['rss_member'] = NULL;
+ }
+
+ if ( is_null( $info['rss_member'] ) )
+ {
+ $blogOwner = \IPS\Db::i()->select( 'blog_member_id', 'blog_blogs', array( "blog_id=?", $info['rss_blog_id'] ) )->first();
+ $info['rss_member'] = $blogOwner;
+ }
+
+ if ( !isset( $info['rss_import_show_link'] ) )
+ {
+ $info['rss_import_show_link'] = "View the full article";
+ }
+
+ $id = $info['rss_id'];
+ unset( $info['rss_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'blog_rss_import', $info );
+ $this->software->app->addLinK( $inserted_id, $id, 'blog_rss_import' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a blog entry attachment
+ *
+ * @param array $info Array of entry attachment information
+ * @param string|NULL $filepath URL/Path to attachment or NULL to use raw file data
+ * @param string|NULL $filedata If $filepath is NULL, this should contain the raw contents of the attachment
+ * @return integer|boolean The ID of the inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath = NULL, $filedata = NULL )
+ {
+ $map['id1_type'] = 'blog_entries';
+ $map['id1_from_parent'] = FALSE;
+ if ( !isset( $info['id2'] ) OR is_null( $info['id2'] ) )
+ {
+ $map['location_key'] = 'blog_Entries';
+ }
+ else
+ {
+ $map['location_key'] = 'blog_Blogs';
+ $map['id2_type'] = 'blog_comments';
+ $map['id2_from_parent'] = FALSE;
+ }
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Board.php b/applications/convert/sources/Library/Board.php
new file mode 100644
index 00000000..5ffdbd43
--- /dev/null
+++ b/applications/convert/sources/Library/Board.php
@@ -0,0 +1,24 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Board extends Forums
+{
+
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Calendar.php b/applications/convert/sources/Library/Calendar.php
new file mode 100644
index 00000000..83a2ad3b
--- /dev/null
+++ b/applications/convert/sources/Library/Calendar.php
@@ -0,0 +1,1116 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Calendar extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'calendar';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertCalendarCalendars':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_calendars',
+ 'step_method' => 'convertCalendarCalendars',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_calendars' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'calendar_calendars',
+ );
+ break;
+
+ case 'convertCalendarEvents':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_events',
+ 'step_method' => 'convertCalendarEvents',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_events' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertCalendarCalendars' ),
+ 'link_type' => 'calendar_events',
+ );
+ break;
+
+ case 'convertCalendarComments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_comments',
+ 'step_method' => 'convertCalendarComments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_event_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertCalendarEvents' ),
+ 'link_type' => 'calendar_event_comments',
+ );
+ break;
+
+ case 'convertCalendarReviews':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_reviews',
+ 'step_method' => 'convertCalendarReviews',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_event_reviews' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertCalendarEvents' ),
+ 'link_type' => 'calendar_event_reviews',
+ );
+ break;
+
+ case 'convertCalendarRsvps':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_rsvps',
+ 'step_method' => 'convertCalendarRsvps',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_event_rsvp' )->first(),
+ 'source_rows' => $this->sofrware->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertCalendarEvents' ),
+ 'link_type' => 'calendar_event_rsvp',
+ );
+ break;
+
+ case 'convertCalendarFeeds':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_calendar_feeds',
+ 'step_method' => 'convertCalendarFeeds',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'calendar_import_feeds' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertCalendarCalendars' ),
+ 'link_type' => 'calendar_import_feeds',
+ );
+ break;
+
+ case 'convertAttachments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_attachments',
+ 'step_method' => 'convertAttachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments', array( "location_key=?", 'calendar_Events' ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertCalendarEvents', 'convertCalendarComments', 'convertCalendarReviews' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertCalendarCalendars':
+ $return['convertCalendarCalendars'] = array( 'calendar_calendars' => NULL );
+ break;
+
+ case 'convertCalendarEvents':
+ $return['convertCalendarEvents'] = array( 'calendar_events' => NULL );
+ break;
+
+ case 'convertCalendarComments':
+ $return['convertCalendarComments'] = array( 'calendar_event_comments' => NULL );
+ break;
+
+ case 'convertCalendarReviews':
+ $return['convertCalendarReviews'] = array( 'calendar_event_reviews' => NULL );
+ break;
+
+ case 'convertCalendarRsvps':
+ $return['convertCalendarRsvps'] = array( 'calendar_event_rsvp' => NULL );
+ break;
+
+ case 'convertCalendarFeeds':
+ $return['convertCalendarFeeds'] = array( 'calender_import_feeds' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $return['convertAttachments'] = array( 'core_attachments' => array( "location_key=?", 'calendar_Events' ) );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a Calendar
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted calendar, or FALSE on failure.
+ */
+ public function convertCalendar( $info=array() )
+ {
+ if ( !isset( $info['cal_id'] ) )
+ {
+ $this->software->app->log( 'calendar_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['cal_title'] ) )
+ {
+ $name = "Calendar {$info['cal_id']}";
+ $this->software->app->log( 'calendar_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['cal_id'] );
+ }
+ else
+ {
+ $name = $info['cal_title'];
+ unset( $info['cal_title'] );
+ }
+
+ if ( !isset( $info['cal_description'] ) )
+ {
+ $desc = '';
+ }
+ else
+ {
+ $desc = $info['cal_description'];
+ unset( $info['cal_description'] );
+ }
+
+ $info['cal_title_seo'] = \IPS\Http\Url::seoTitle( $name );
+
+ /* Zero Defaults */
+ foreach( array( 'cal_moderate', 'cal_comment_moderate', 'cal_allow_reviews', 'cal_review_moderate' ) AS $zeroDefault )
+ {
+ if ( !isset( $info[ $zeroDefault ] ) )
+ {
+ $info[ $zeroDefault ] = 0;
+ }
+ }
+
+ if ( !isset( $info['cal_allow_comments'] ) )
+ {
+ $info['cal_allow_comments'] = 1;
+ }
+
+ if ( !isset( $info['cal_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(position)', 'calendar_calendars' )->first();
+ $info['cal_position'] = $position + 1;
+ }
+
+ if ( !isset( $info['cal_color'] ) )
+ {
+ $genericCalendar = new \IPS\calendar\Calendar;
+ $info['cal_color'] = $genericCalendar->_generateColor();
+ }
+
+ $id = $info['cal_id'];
+ unset( $info['cal_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_calendars', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_calendars' );
+
+ \IPS\Lang::saveCustom( 'calendar', "calendar_calendar_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'calendar', "calendar_calendar_{$inserted_id}_desc", $desc );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an event
+ *
+ * @param array $info Data to insert
+ * @param string|NULL $filepath Path to the event cover photo, or NULL.
+ * @param string|NULL $filedata Cover photo binary data, or NULL
+ * @return integer|boolean The ID of the newly inserted event, or FALSE on failure.
+ */
+ public function convertCalendarEvent( $info=array(), $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['event_id'] ) )
+ {
+ $this->software->app->log( 'calendar_event_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['event_calendar_id'] ) )
+ {
+ try
+ {
+ $info['event_calendar_id'] = $this->software->app->getLink( $info['event_calendar_id'], 'calendar_calendars' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_event_missing_calendar', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['event_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_event_missing_calendar', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['event_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['event_member_id'] ) )
+ {
+ try
+ {
+ $info['event_member_id'] = $this->software->app->getLink( $info['event_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['event_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['event_member_id'] = 0;
+ }
+
+ if ( !isset( $info['event_title'] ) )
+ {
+ $event['event_title'] = "Untitled Event {$info['event_id']}";
+ $this->software->app->log( 'calendar_event_missing_title', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['event_id'] );
+ }
+
+ /* Maye we can do this for other apps too? I have seen some complaints where content can be intentionally left blank in some softwares */
+ if ( empty( $info['event_content'] ) )
+ {
+ $event['event_content'] = "{$info['event_title']}
";
+ $this->software->app->log( 'calendar_event_missing_content', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['event_id'] );
+ }
+
+ /* Zero Defaults! */
+ foreach( array( 'event_comments', 'event_rsvp', 'event_rating', 'event_sequence', 'event_all_day', 'event_reviews', 'event_locked', 'event_featured', 'event_queued_comments', 'event_hidden_comments', 'event_unapproved_reviews', 'event_hidden_reviews' ) AS $zeroDefault )
+ {
+ if ( !isset( $info[ $zeroDefault ] ) )
+ {
+ $info[ $zeroDefault ] = 0;
+ }
+ }
+
+ if ( isset( $info['event_saved'] ) )
+ {
+ if ( $info['event_saved'] instanceof \IPS\DateTime )
+ {
+ $info['event_saved'] = $info['event_saved']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['event_saved'] = time();
+ }
+
+ if ( isset( $info['event_lastupdated'] ) )
+ {
+ if ( $info['event_lastupdated'] instanceof \IPS\DateTime )
+ {
+ $info['event_lastupdated'] = $info['event_lastupdated']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['event_lastupdated'] = $info['event_saved'];
+ }
+
+ if ( isset( $info['event_recurring'] ) )
+ {
+ /* If we have an array, pass off to ICSParser so we can build it */
+ if ( is_array( $info['event_recurring'] ) )
+ {
+ $info['event_recurring'] = \IPS\calendar\Icalendar\ICSParser::buildRrule( $info['event_recurring'] );
+ }
+ else
+ {
+ /* If we didn't, make sure it's valid */
+ try
+ {
+ \IPS\calendar\Icalendar\ICSParser::parserRrule( $info['event_recurring'] );
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( 'calendar_event_recurring_invalid', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['event_id'] );
+ $info['event_recurring'] = NULL;
+ }
+ }
+ }
+ else
+ {
+ $info['event_recurring'] = NULL;
+ }
+
+ if ( isset( $info['event_start_date'] ) )
+ {
+ if ( $info['event_start_date'] instanceof \IPS\calendar\Date )
+ {
+ $info['event_start_date'] = $info['event_start_date']->mysqlDatetime();
+ }
+ else if ( $info['event_start_date'] instanceof \IPS\DateTime )
+ {
+ $info['event_start_date'] = \IPS\calendar\Date::create( (string) $info['event_start_date'] )->mysqlDatetime();
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_event_missing_start_date', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['event_end_date'] ) )
+ {
+ if ( $info['event_end_date'] instanceof \IPS\calendar\Date )
+ {
+ $info['event_end_date'] = $info['event_end_date']->mysqlDatetime();
+ }
+ else if ( $info['event_end_date'] instanceof \IPS\DateTime )
+ {
+ $info['event_end_date'] = \IPS\calendar\Date::create( (string) $info['event_end_date'] )->mysqlDatetime();
+ }
+ }
+ else
+ {
+ $info['event_end_date'] = NULL;
+ }
+
+ $info['event_title_seo'] = \IPS\Http\Url::seoTitle( $info['event_title'] );
+ $info['event_post_key'] = md5( microtime() );
+
+ if ( !isset( $info['event_ip_address'] ) OR filter_var( $info['event_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['event_ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['event_last_comment'] ) )
+ {
+ if ( $info['event_last_comment'] instanceof \IPS\DateTime )
+ {
+ $info['event_last_comment'] = $info['event_last_comment']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['event_last_comment'] = $info['event_saved'];
+ }
+
+ if ( isset( $info['event_last_review'] ) )
+ {
+ if ( $info['event_last_review'] instanceof \IPS\DateTime )
+ {
+ $info['event_last_review'] = $info['event_last_review']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['event_last_review'] = $info['event_saved'];
+ }
+
+ if ( !isset( $info['event_approved'] ) )
+ {
+ $info['event_approved'] = 1;
+ }
+
+ if ( isset( $info['event_approved_by'] ) )
+ {
+ try
+ {
+ $info['event_approved_by'] = $this->software->app->getLink( $info['event_approved_by'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['event_approved_by'] = NULL;
+ }
+ }
+ else
+ {
+ $info['event_approved_by'] = NULL;
+ }
+
+ if ( isset( $info['event_approved_on'] ) )
+ {
+ if ( $info['event_approved_on'] instanceof \IPS\DateTime )
+ {
+ $info['event_approved_on'] = $info['event_approved_on']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['event_approved_on'] = NULL;
+ }
+
+ if ( isset( $info['event_location'] ) )
+ {
+ if ( is_array( $info['event_location'] ) AND isset( $info['event_location']['lat'] ) AND isset( $info['event_location']['long'] ) )
+ {
+ $info['event_location'] = (string) \IPS\GeoLocation::getFromLatLong( $info['event_location']['lat'], $info['event_location']['long'] );
+ }
+ else if ( $info['event_location'] instanceof \IPS\GeoLocation )
+ {
+ $info['event_location'] = (string) $info['event_location'];
+ }
+ }
+ else
+ {
+ $info['event_location'] = NULL;
+ }
+
+ if ( !isset( $info['event_rsvp_limit'] ) )
+ {
+ $info['event_rsvp_limit'] = -1;
+ }
+
+ if ( isset( $info['event_album'] ) )
+ {
+ try
+ {
+ $info['event_album'] = $this->software->app->getSiblingLink( $info['event_album'], 'gallery_albums', 'gallery' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['event_album'] = NULL;
+ }
+ }
+ else
+ {
+ $info['event_album'] = NULL;
+ }
+
+ if ( isset( $info['event_cover_photo'] ) AND ( !is_null( $filepath ) OR !is_null( $filedata ) ) )
+ {
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = file_get_contents( $filepath );
+ }
+
+ $file = \IPS\File::create( 'calendar_Events', $info['event_cover_photo'], $filedata );
+ $info['event_cover_photo'] = (string) $file;
+ }
+ catch( \Exception $e )
+ {
+ $info['event_cover_photo'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['event_cover_photo'] = NULL;
+ }
+ }
+ else
+ {
+ $info['event_cover_photo'] = NULL;
+ }
+
+ $id = $info['event_id'];
+ unset( $info['event_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_events', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_events' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a comment
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted comment, or FALSE on failure.
+ */
+ public function convertCalendarComment( $info=array() )
+ {
+ if ( !isset( $info['comment_id'] ) )
+ {
+ $this->software->app->log( 'calendar_event_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_eid'] ) )
+ {
+ try
+ {
+ $info['comment_eid'] = $this->software->app->getLink( $info['comment_eid'], 'calendar_events' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_event_comment_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_event_comment_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['comment_text'] ) )
+ {
+ $this->software->app->log( 'calendar_event_comment_empty', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_mid'] ) )
+ {
+ try
+ {
+ $info['comment_mid'] = $this->software->app->getLink( $info['comment_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_mid'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_mid'] = 0;
+ }
+
+ if ( isset( $info['comment_date'] ) )
+ {
+ if ( $info['comment_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_date'] = $info['comment_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_date'] = time();
+ }
+
+ if ( !isset( $info['comment_approved'] ) )
+ {
+ $info['comment_approved'] = 1;
+ }
+
+ if ( !isset( $info['comment_append_edit'] ) )
+ {
+ $info['comment_append_edit'] = 0;
+ }
+
+ if ( isset( $info['comment_edit_time'] ) )
+ {
+ if ( $info['comment_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['comment_edit_time'] = $info['comment_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_edit_time'] = 0;
+ }
+
+ if ( !isset( $info['comment_edit_name'] ) )
+ {
+ $info['comment_edit_name'] = NULL;
+ }
+
+ if ( !isset( $info['comment_ip_address'] ) OR filter_var( $info['comment_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['comment_ip_address'] = '127.0.0.1';
+ }
+
+ if ( !isset( $info['comment_author'] ) )
+ {
+ $author = \IPS\Member::load( $info['comment_mid'] );
+
+ if ( $author->member_id )
+ {
+ $info['comment_author'] = $author->name;
+ }
+ else
+ {
+ $info['comment_author'] = "Guest";
+ }
+ }
+
+ $id = $info['comment_id'];
+ unset( $info['comment_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_event_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_event_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a review
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted review, or FALSE on failure.
+ */
+ public function convertCalendarReview( $info=array() )
+ {
+ if ( !isset( $info['review_id'] ) )
+ {
+ $this->software->app->log( 'calendar_event_review_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_eid'] ) )
+ {
+ try
+ {
+ $info['review_eid'] = $this->software->app->getLink( $info['review_eid'], 'calendar_events' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_event_review_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_event_review_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ /* Unlike comments, guests cannot review */
+ if ( isset( $info['review_mid'] ) )
+ {
+ try
+ {
+ $info['review_mid'] = $this->software->app->getLink( $info['review_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_event_review_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_event_review_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['review_text'] ) )
+ {
+ $this->software->app->log( 'calendar_event_review_empty', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ /* This seems silly, but we really do need a rating */
+ if ( !isset( $info['review_rating'] ) OR $info['review_rating'] < 1 )
+ {
+ $this->software->app->log( 'calendar_event_review_invalid_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['review_append_edit'] ) )
+ {
+ $info['review_append_edit'] = 0;
+ }
+
+ if ( isset( $info['review_edit_time'] ) )
+ {
+ if ( $info['review_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['review_edit_time'] = $info['review_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_edit_time'] = time();
+ }
+
+ if ( !isset( $info['review_edit_name'] ) )
+ {
+ $info['review_edit_name'] = NULL;
+ }
+
+ if ( isset( $info['review_date'] ) )
+ {
+ if ( $info['review_date'] instanceof \IPS\DateTime )
+ {
+ $info['review_date'] = $info['review_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_date'] = time();
+ }
+
+ if ( !isset( $info['review_ip'] ) OR filter_var( $info['review_ip'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['review_ip'] = '127.0.0.1';
+ }
+
+ if ( !isset( $info['review_author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['review_mid'] );
+
+ if ( $author->member_id )
+ {
+ $info['review_author_name'] = $author->name;
+ }
+ else
+ {
+ $info['review_author_name'] = "Guest";
+ }
+ }
+
+ if ( isset( $info['review_votes_data'] ) )
+ {
+ if ( !is_array( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_data'] = json_decode( $info['review_votes_data'], TRUE );
+ }
+
+ $newVoters = array();
+ if ( !is_null( $info['review_votes_data'] ) AND count( $info['review_votes_data'] ) )
+ {
+ foreach( $info['review_votes_data'] as $member => $vote )
+ {
+ try
+ {
+ $memberId = $this->software->app->getLink( $member, 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ $newVoters[ $memberId ] = $vote;
+ }
+ }
+
+ if ( count( $newVoters ) )
+ {
+ $info['review_votes_data'] = json_encode( $newVoters );
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+
+ if ( !isset( $info['review_votes'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes'] = 0;
+ }
+ else
+ {
+ $info['review_votes'] = count( json_decode( $info['review_votes_data'], TRUE ) );
+ }
+ }
+
+ if ( !isset( $info['review_votes_helpful'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_helpful'] = 0;
+ }
+ else
+ {
+ $helpful = 0;
+ foreach( json_decode( $info['review_votes_data'], TRUE ) AS $member => $vote )
+ {
+ if ( $vote == 1 )
+ {
+ $helpful += 1;
+ }
+ }
+
+ $info['review_votes_helpful'] = $helpful;
+ }
+ }
+
+ if ( !isset( $info['review_approved'] ) )
+ {
+ $info['review_approved'] = 1;
+ }
+
+ $id = $info['review_id'];
+ unset( $info['review_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_event_reviews', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_event_reviews' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an RSVP
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted RSVP, or FALSE on failure.
+ */
+ public function convertCalendarRsvp( $info=array() )
+ {
+ $hasId = TRUE;
+ if ( !isset( $info['rsvp_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( isset( $info['rsvp_event_id'] ) )
+ {
+ try
+ {
+ $info['rsvp_event_id'] = $this->software->app->getLink( $info['rsvp_event_id'], 'calendar_events' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_rsvp_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['rsvp_id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_rsvp_missing_event', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['rsvp_id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['rsvp_member_id'] ) )
+ {
+ try
+ {
+ $info['rsvp_member_id'] = $this->software->app->getLink( $info['rsvp_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_rsvp_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['rsvp_id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_rsvp_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['rsvp_id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['rsvp_date'] ) )
+ {
+ if ( $info['rsvp_date'] instanceof \IPS\DateTime )
+ {
+ $info['rsvp_date'] = $info['rsvp_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['rsvp_date'] = time();
+ }
+
+ if ( !isset( $info['rsvp_response'] ) OR !in_array( $info['rsvp_response'], array( 0, 1, 2 ) ) )
+ {
+ $this->software->app->log( 'calendar_rsvp_invalid_response', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['rsvp_id'] : NULL );
+ return FALSE;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['rsvp_id'];
+ unset( $info['rsvp_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_event_rsvp', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_event_rsvp' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Calendar Import Feed
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted feed, or FALSE on failure.
+ */
+ public function convertCalendarFeed( $info=array() )
+ {
+ if ( !isset( $info['feed_id'] ) )
+ {
+ $this->software->app->log( 'calendar_feed_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['feed_title'] ) )
+ {
+ $info['feed_title'] = "Untitled Feed {$info['feed_id']}";
+ $this->software->app->log( 'calendar_feed_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ }
+
+ if ( !isset( $info['feed_url'] ) OR filter_var( $info['feed_url'], FILTER_VALIDATE_URL ) === FALSE )
+ {
+ $this->software->app->log( 'calendar_feed_invalid_url', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['feed_added'] ) )
+ {
+ if ( $info['feed_added'] instanceof \IPS\DateTime )
+ {
+ $info['feed_added'] = $info['feed_added']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['feed_added'] = time();
+ }
+
+ if ( isset( $info['feed_lastupdated'] ) )
+ {
+ if ( $info['feed_lastupdated'] instanceof \IPS\DateTime )
+ {
+ $info['feed_lastupdated'] = $info['feed_lastupdated']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['feed_lastupdated'] = time();
+ }
+
+ if ( isset( $info['feed_calendar_id'] ) )
+ {
+ try
+ {
+ $info['feed_calendar_id'] = $this->software->app->getLink( $info['feed_calendar_id'], 'calendar_calendars' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_feed_missing_calendar', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_feed_missing_calendar', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['feed_member_id'] ) )
+ {
+ try
+ {
+ $info['feed_member_id'] = $this->software->app->getLink( $info['feed_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'calendar_feed_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'calendar_feed_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['feed_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['feed_last_run'] ) )
+ {
+ if ( $info['feed_last_run'] instanceof \IPS\DateTime )
+ {
+ $info['feed_last_run'] = $info['feed_last_run']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['feed_last_run'] = time();
+ }
+
+ if ( !isset( $info['feed_allow_rsvp'] ) )
+ {
+ $info['feed_allow_rsvp'] = 0;
+ }
+
+ $id = $info['feed_id'];
+ unset( $info['feed_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'calendar_import_feeds', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'calendar_import_feeds' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Attachment Map Data
+ * @param string|NULL $filepath Path to the file, or NULL.
+ * @param string|NULL $filedata Binary data for the file, or NULL.
+ * @return integer|boolean The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ $map['location_key'] = 'calendar_Calendar';
+ $map['id1_type'] = 'calendar_events';
+ $map['id1_from_parent'] = FALSE;
+ $map['id2_from_parent'] = FALSE;
+ /* Some set up */
+ if ( !isset( $info['id3'] ) )
+ {
+ $info['id3'] = NULL;
+ }
+
+ if ( is_null( $info['id3'] ) OR $info['id3'] != 'review' )
+ {
+ $map['id2_type'] = 'calendar_event_comments';
+ }
+ else
+ {
+ $map['id2_type'] = 'calendar_event_reviews';
+ }
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Ccs.php b/applications/convert/sources/Library/Ccs.php
new file mode 100644
index 00000000..a09060d3
--- /dev/null
+++ b/applications/convert/sources/Library/Ccs.php
@@ -0,0 +1,24 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Ccs extends Cms
+{
+
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Cms.php b/applications/convert/sources/Library/Cms.php
new file mode 100644
index 00000000..b711a303
--- /dev/null
+++ b/applications/convert/sources/Library/Cms.php
@@ -0,0 +1,2663 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * @note We must extend the Core Library here so we can access methods like convertAttachment, convertFollow, etc
+ */
+class _Cms extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'cms';
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to complete this conversion
+ *
+ * @return string
+ */
+ public function getPostConversionInformation()
+ {
+ return parent::getPostConversionInformation() . \IPS\Member::loggedIn()->language()->addToStack( 'convert_cms_info_message' );
+ }
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertCmsBlocks':
+ $dependencies = array();
+ if ( array_key_exists( 'convertCmsContainers', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertCmsContainers';
+ }
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_blocks',
+ 'step_method' => 'convertCmsBlocks',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_blocks' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'cms_blocks',
+ );
+ break;
+
+ case 'convertCmsContainers':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_containers',
+ 'step_method' => 'convertCmsContainers',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_containers' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'cms_containers',
+ );
+ break;
+
+ case 'convertCmsDatabases':
+ $dependencies = array();
+ if ( array_key_exists( 'convertCmsPages', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertCmsPages';
+ }
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_databases',
+ 'step_method' => 'convertCmsDatabases',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_databases' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'cms_databases',
+ );
+ break;
+
+ case 'convertCmsDatabaseCategories':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_database_categories',
+ 'step_method' => 'convertCmsDatabaseCategories',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_database_categories' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertCmsDatabases' ),
+ 'link_type' => 'cms_database_categories',
+ );
+ break;
+
+ case 'convertCmsDatabaseRecords':
+ $count = 0;
+ $links = array();
+ foreach( \IPS\Db::i()->select( 'database_id', 'cms_databases' ) AS $database )
+ {
+ $count += \IPS\Db::i()->select( 'COUNT(*)', "cms_custom_database_{$database}" )->first();
+ $links[] = "cms_custom_database_{$database}";
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_database_records',
+ 'step_method' => 'convertCmsDatabaseRecords',
+ 'ips_rows' => $count,
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertCmsDatabases', 'convertCmsDatabaseCategories' ),
+ 'link_type' => $links,
+ );
+ break;
+
+ case 'convertCmsDatabaseComments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_database_comments',
+ 'step_method' => 'convertCmsDatabaseComments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_database_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertCmsDatabaseRecords' ),
+ 'link_type' => 'cms_database_comments',
+ );
+ break;
+
+ case 'convertCmsDatabaseReviews':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_database_reviews',
+ 'step_method' => 'convertCmsDatabaseReviews',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_database_reviews' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertCmsDatabaseRecords' ),
+ 'link_type' => 'cms_database_reviews',
+ );
+ break;
+
+ case 'convertCmsFolders':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_folders',
+ 'step_method' => 'convertCmsFolders',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_folders' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'cms_folders',
+ );
+ break;
+
+ case 'convertCmsMedia':
+ $dependencies = array();
+ if ( array_key_exists( 'convertCmsMediaFolders', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertCmsMediaFolders';
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_media',
+ 'step_method' => 'convertCmsMedia',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_media' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'cms_media',
+ );
+ break;
+
+ case 'convertCmsMediaFolders':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_media_folders',
+ 'step_method' => 'convertCmsMediaFolders',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_media_folders' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'cms_media_folders',
+ );
+ break;
+
+ case 'convertCmsPages':
+ $dependencies = array();
+ if ( array_key_exists( 'convertCmsFolders', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertCmsFolders';
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_pages',
+ 'step_method' => 'convertCmsPages',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'cms_pages' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'cms_pages',
+ );
+ break;
+
+ case 'convertAttachments':
+ $in = array();
+ foreach( \IPS\Db::i()->select( 'database_id', 'cms_databases' ) AS $database )
+ {
+ $in[] = $database;
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_cms_attachments',
+ 'step_method' => 'convertAttachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( \IPS\Db::i()->in( 'id3', $in ) . " AND location_key=?", 'cms_Records' ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertCmsDatabaseRecords', 'convertCmsDatabaseComments', 'convertCmsDatabaseReviews' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertCmsFolders':
+ $return['convertCmsFolders'] = array( 'cms_folders' => NULL );
+ break;
+
+ case 'convertCmsPages':
+ $return['convertCmsPages'] = array( 'cms_pages' => NULL );
+ break;
+
+ case 'convertCmsContainers':
+ $return['convertCmsContainers'] = array( 'cms_containers' => NULL );
+ break;
+
+ case 'convertCmsBlocks':
+ $return['convertCmsBlocks'] = array( 'cms_blocks' => NULL );
+ break;
+
+ case 'convertCmsDatabases':
+ if ( $method == $k )
+ {
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'cms_databases' ), 'IPS\cms\Databases' ) AS $database )
+ {
+ $database->delete();
+ }
+ }
+
+ return array();
+ break;
+
+ case 'convertCmsDatabaseCategories':
+ $return['convertCmsDatabaseDategories'] = array( 'cms_database_categories' => NULL );
+ break;
+
+ case 'convertCmsDatabaseRecords':
+ if ( $method == $k )
+ {
+ $toReturn = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'cms_databases' ), 'IPS\cms\Databases' ) AS $database )
+ {
+ $toReturn["cms_custom_database_{$database->_id}"] = NULL;
+ }
+ $return['convertCmsDatabaseRecords'] = $toReturn;
+ }
+ break;
+
+ case 'convertCmsDatabaseComments':
+ $return['convertCmsDatabaseComments'] = array( 'cms_database_comments' => NULL );
+ break;
+
+ case 'convertCmsDatabaseReviews':
+ $return['convertCmsDatabaseReviews'] = array( 'cms_database_reviews' => NULL );
+ break;
+
+ case 'convertCmsMediaFolders':
+ $return['convertCmsMediaFolders'] = array( 'cms_media_folders' => NULL );
+ break;
+
+ case 'convertCmsMedia':
+ $return['convertCmsMedia'] = array( 'cms_media' => NULL );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a Folder
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted folder, or FALSE on failure.
+ */
+ public function convertCmsFolder( $info=array() )
+ {
+ if ( !isset( $info['folder_id'] ) )
+ {
+ $this->software->app->log( 'cms_folder_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['folder_name'] ) )
+ {
+ $info['folder_name'] = "untitledfolder{$info['folder_id']}";
+ }
+
+ if ( isset( $info['folder_parent_id'] ) )
+ {
+ try
+ {
+ $info['folder_parent_id'] = $this->software->app->getLink( $info['folder_parent_id'], 'cms_folders' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['folder_conv_parent'] = $info['folder_parent_id'];
+ }
+ }
+ else
+ {
+ $info['folder_parent_id'] = 0;
+ }
+
+ if ( isset( $info['folder_last_modified'] ) )
+ {
+ if ( $info['folder_last_modified'] instanceof \IPS\DateTime )
+ {
+ $info['folder_last_modified'] = $info['folder_last_modified']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['folder_last_modified'] = time();
+ }
+
+ if ( !isset( $info['folder_path'] ) )
+ {
+ $info['folder_path'] = $info['folder_name'];
+ }
+
+ $id = $info['folder_id'];
+ unset( $info['folder_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_folders', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_folders' );
+
+ \IPS\Db::i()->update( 'cms_folders', array( 'folder_parent_id' => $inserted_id ), array( "folder_conv_parent=? AND folder_conv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Page
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted page, or FALSE on failure.
+ */
+ public function convertCmsPage( $info=array() )
+ {
+ if ( !isset( $info['page_id'] ) )
+ {
+ $this->software->app->log( 'cms_page_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Get these out of the way - we won't know what they are, can't convert them, or are unused */
+ $info['page_theme'] = 0;
+ $info['page_type'] = 'html';
+ $info['page_template'] = '';
+ $info['page_quicknav'] = 1;
+ $info['page_wrapper_template'] = '_none_';
+ $info['page_has_error'] = 0;
+ $info['page_js_css_ids'] = '';
+ $info['page_js_css_objects'] = json_encode( array( 'css' => NULL, 'js' => NULL ) );
+
+ if ( !isset( $info['page_name'] ) )
+ {
+ $info['page_name'] = "Untitled Page {$info['page_id']}";
+ }
+
+ if ( !isset( $info['page_seo_name'] ) )
+ {
+ $info['page_seo_name'] = \IPS\Http\Url::seoTitle( $info['page_name'] );
+ }
+
+ if ( !isset( $info['page_content'] ) )
+ {
+ $info['page_content'] = '';
+ }
+
+ if ( isset( $info['page_meta_keywords'] ) )
+ {
+ if ( is_array( $info['page_meta_keywords'] ) )
+ {
+ $info['page_meta_keywords'] = implode( ',', $info['page_meta_keywords'] );
+ }
+ }
+ else
+ {
+ $info['page_meta_keywords'] = '';
+ }
+
+ if ( !isset( $info['page_meta_description'] ) )
+ {
+ $info['page_meta_description'] = '';
+ }
+
+ if ( !isset( $info['page_ipb_wrapper'] ) )
+ {
+ $info['page_ipb_wrapper'] = 1;
+ }
+
+ if ( !isset( $info['page_title'] ) )
+ {
+ $info['page_title'] = $info['page_name'];
+ }
+
+ if ( isset( $info['page_folder_id'] ) )
+ {
+ try
+ {
+ $info['page_folder_id'] = $this->software->app->getLink( $info['page_folder_id'], 'cms_folders' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['page_folder_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['page_folder_id'] = 0;
+ }
+
+ if ( !isset( $info['page_full_path'] ) )
+ {
+ try
+ {
+ $info['page_full_path'] = \IPS\Db::i()->select( 'folder_path', 'cms_folders', array( "folder_id=?", $info['page_folder_id'] ) )->first() . '/' . $info['page_seo_name'];
+ }
+ catch( \UnderflowException $e )
+ {
+ $info['page_full_path'] = $info['page_seo_name'];
+ }
+ }
+ else
+ {
+ $info['page_full_path'] = $info['page_seo_name'];
+ }
+
+ if ( !isset( $info['page_show_sidebar'] ) )
+ {
+ $info['page_show_sidebar'] = 0;
+ }
+
+ if ( !isset( $info['page_default'] ) )
+ {
+ $info['page_default'] = 0;
+ }
+
+ $id = $info['page_id'];
+ unset( $info['page_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_pages', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_pages' );
+ \IPS\Lang::saveCustom( 'cms', "cms_page_{$inserted_id}", $info['page_name'] );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a block container
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted container, or FALSE on failure.
+ */
+ public function convertCmsContainer( $info=array() )
+ {
+ if ( !isset( $info['container_id'] ) )
+ {
+ $this->software->app->log( 'cms_container_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['container_name'] ) )
+ {
+ $info['container_name'] = "Converted Blocks";
+ $this->software->app->log( 'cms_container_no_name', __METHOD__, \IPS\convert\ApP::LOG_NOTICE, $info['container_id'] );
+ }
+
+ if ( isset( $info['container_parent_id'] ) )
+ {
+ try
+ {
+ $info['container_parent_id'] = $this->software->app->getLink( $info['container_parent_id'], 'cms_containers' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['container_conv_parent'] = $info['container_parent_id'];
+ }
+ }
+ else
+ {
+ $info['container_parent_id'] = 0;
+ }
+
+ if ( !isset( $info['container_type'] ) OR !in_array( $info['container_type'], array( 'block' ) ) )
+ {
+ $info['container_type'] = 'block';
+ }
+
+ if ( !isset( $info['container_order'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(container_order)', 'cms_containers' )->first();
+
+ $info['container_order'] = $position + 1;
+ }
+
+ if ( !isset( $info['container_key'] ) )
+ {
+ $info['container_key'] = \IPS\Http\Url::seoTitle( $info['container_name'] );
+ }
+
+ $id = $info['container_id'];
+ unset( $info['container_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_containers', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_containers' );
+
+ \IPS\Db::i()->update( 'cms_containers', array( "container_parent_id" => $inserted_id ), array( "container_conv_parent=? AND container_conv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a block
+ * @note These probably won't really work unless going from IPS4 to IPS4, but we still support them anyway so they can be refactored.
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted block, or FALSE on failure.
+ */
+ public function convertCmsBlock( $info )
+ {
+ if ( !isset( $info['block_id'] ) )
+ {
+ $this->software->app->log( 'cms_block_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['block_name'] ) )
+ {
+ $name = $info['block_name'];
+ unset( $info['block_name'] );
+ }
+ else
+ {
+ $name = "Untitled Block {$info['block_id']}";
+ }
+
+ if ( isset( $info['block_description'] ) )
+ {
+ $desc = $info['block_description'];
+ unset( $info['block_description'] );
+ }
+ else
+ {
+ $desc = '';
+ }
+
+ if ( !isset( $info['block_key'] ) )
+ {
+ $info['block_key'] = \IPS\Http\Url::seoTitle( $name );
+ }
+
+ /* We cannot do these - force defaults */
+ $info['block_template'] = 0;
+ $info['block_template_params'] = '';
+ $info['block_type'] = 'custom';
+ $info['block_plugin'] = NULL;
+ $info['block_plugin_config'] = NULL;
+ $info['block_plugin_app'] = NULL;
+ $info['block_plugin_plugin'] = NULL;
+
+ if ( isset( $info['block_config'] ) )
+ {
+ if ( !is_array( $info['block_config'] ) )
+ {
+ $info['block_config'] = json_decode( $info['block_config'], TRUE );
+ }
+
+ if ( !$info['block_config'] )
+ {
+ $info['block_config'] = array( 'editor' => 'html' );
+ }
+ else
+ {
+ if ( !isset( $info['block_config']['editor'] ) OR !in_array( $info['block_config']['editor'], array( 'editor', 'html', 'php' ) ) )
+ {
+ $info['block_config']['editor'] = 'html';
+ }
+ }
+ }
+ else
+ {
+ $info['block_config'] = array( 'editor' => 'html' );
+ }
+
+ $info['block_config'] = json_encode( $info['block_config'] );
+
+ if ( !isset( $info['block_content'] ) )
+ {
+ $info['block_content'] = NULL;
+ }
+
+ if ( !isset( $info['block_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(block_position)', 'cms_blocks' )->first();
+
+ $info['block_position'] = $position + 1;
+ }
+
+ if ( isset( $info['block_category'] ) )
+ {
+ try
+ {
+ $info['block_category'] = $this->software->app->getLink( $info['block_category'], 'cms_containers' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['block_category'] = $this->_orphanedBlocksCategory();
+ }
+ }
+ else
+ {
+ $info['block_category'] = $this->_orphanedBlocksCategory();
+ }
+
+ if ( !isset( $info['block_cache'] ) )
+ {
+ $info['block_cache'] = 0;
+ }
+
+ $id = $info['block_id'];
+ unset( $info['block_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_blocks', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_blocks' );
+
+ \IPS\Lang::saveCustom( 'cms', "content_block_name_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'cms', "content_block_name_{$inserted_id}_desc", $desc );
+
+ return $inserted_id;
+ }
+
+ /**
+ * @brief Orphaned Blocks Container
+ */
+ protected $orphanedBlockContainer = NULL;
+
+ /**
+ * Returns a category to store blocks that do not have one, creating one if needed.
+ *
+ * @return integer The ID.
+ */
+ protected function _orphanedBlocksCategory()
+ {
+ if ( $this->orphanedBlockContainer === NULL )
+ {
+ try
+ {
+ $this->orphanedBlockContainer = $this->software->app->getLink( '__orphaned__', 'cms_containers' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->orphanedBlockContainer = $this->convertCmsContainer( array(
+ 'container_id' => '__orphaned__',
+ 'container_name' => "Converted Blocks",
+ ) );
+ }
+ }
+
+ return $this->orphanedBlockContainer;
+ }
+
+ /**
+ * Convert a database
+ *
+ * @param array $info Data to insert
+ * @param array $fields Custom Field Data
+ * @return integer|boolean The ID of the newly inserted database, or FALSE on failure.
+ */
+ public function convertCmsDatabase( $info=array(), $fields=array() )
+ {
+ /* This is about to get caaaraaaaaazy */
+ if ( !isset( $info['database_id'] ) )
+ {
+ $this->software->app->log( 'cms_database_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* No fields - we need at least one */
+ if ( !count( $fields ) )
+ {
+ $this->software->app->log( 'cms_database_no_fields', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['database_id'] );
+ return FALSE;
+ }
+
+ /* Language Stuff */
+ if ( isset( $info['database_name'] ) )
+ {
+ $name = $info['database_name'];
+ unset( $info['database_name'] );
+ }
+ else
+ {
+ $name = "Database {$info['database_id']}";
+ }
+
+ if ( isset( $info['database_description'] ) )
+ {
+ $desc = $info['database_description'];
+ unset( $info['database_description'] );
+ }
+ else
+ {
+ $desc = '';
+ }
+
+ if ( isset( $info['database_sln'] ) )
+ {
+ $sln = $info['database_sln'];
+ unset( $info['database_sln'] );
+ }
+ else
+ {
+ $sln = 'record';
+ }
+
+ if ( isset( $info['database_pln'] ) )
+ {
+ $pln = $info['database_pln'];
+ unset( $info['database_pln'] );
+ }
+ else
+ {
+ $pln = 'records';
+ }
+
+ if ( isset( $info['database_scn'] ) )
+ {
+ $scn = $info['database_scn'];
+ unset( $info['database_scn'] );
+ }
+ else
+ {
+ $scn = "Record";
+ }
+
+ if ( isset( $info['database_pcn'] ) )
+ {
+ $pcn = $info['database_pcn'];
+ unset( $info['database_pcn'] );
+ }
+ else
+ {
+ $pcn = "Records";
+ }
+
+ if ( isset( $info['database_ia'] ) )
+ {
+ $ia = $info['database_ia'];
+ unset( $info['database_ia'] );
+ }
+ else
+ {
+ $ia = "a record";
+ }
+
+ if ( !isset( $info['database_key'] ) )
+ {
+ $info['database_key'] = \IPS\Http\Url::seoTitle( $name );
+ }
+
+ /* Zero Defaults */
+ foreach( array( 'database_record_count', 'database_all_editable', 'database_comment_approve', 'database_record_approve', 'database_tags_enabled', 'database_tags_noprefixes', 'database_cat_index_type' ) AS $zeroDefault )
+ {
+ if ( !isset( $info[ $zeroDefault ] ) )
+ {
+ $info[ $zeroDefault ] = 0;
+ }
+ }
+
+ /* One Defaults */
+ foreach( array( 'database_revisions', 'database_rss', 'database_comment_bump', 'database_search', 'database_use_categories' ) as $oneDefault )
+ {
+ if ( !isset( $info[ $oneDefault ] ) )
+ {
+ $info[ $oneDefault ] = 1;
+ }
+ }
+
+ if ( isset( $info['database_options'] ) )
+ {
+ if ( is_array( $info['database_options'] ) )
+ {
+ $bitoptions = 0;
+
+ foreach( \IPS\cms\Databases::$bitOptions as $key => $value )
+ {
+ if ( isset( $info['database_options'][$key] ) AND $info['database_options'][$key] )
+ {
+ $bitoptions += $value;
+ }
+ }
+
+ $info['database_options'] = $bitoptions;
+ }
+ }
+ else
+ {
+ $info['database_options'] = 1;
+ }
+
+ if ( isset( $info['database_fixed_field_perms'] ) )
+ {
+ if ( is_array( $info['database_fixed_field_perms'] ) )
+ {
+ $info['database_fixed_field_perms'] = json_encode( $info['database_fixed_field_perms'] );
+ }
+ }
+ else
+ {
+ $info['database_fixed_field_perms'] = json_encode( array(
+ 'record' => array(
+ 'visible' => TRUE,
+ 'perm_view' => '*',
+ 'perm_2' => '*',
+ 'perm_3' => '*'
+ ),
+ 'record_image' => array(
+ 'visible' => TRUE,
+ 'perm_view' => '*',
+ 'perm_2' => '*',
+ 'perm_3' => '*'
+ )
+ ) );
+ }
+
+ if ( isset( $info['database_featured_settings'] ) )
+ {
+ if ( is_array( $info['database_featured_settings'] ) )
+ {
+ $info['database_featured_settings'] = json_encode( $info['database_featured_settings'] );
+ }
+ }
+ else
+ {
+ $info['database_featured_settings'] = json_encode( array(
+ 'featured' => FALSE,
+ 'perpage' => 10,
+ 'pagination' => FALSE,
+ 'sort' => 'record_publish_date',
+ 'direction' => 'desc',
+ 'categories' => 0,
+ ) );
+ }
+
+ if ( isset( $info['database_fixed_field_settings'] ) )
+ {
+ if ( is_array( $info['database_fixed_field_settings'] ) )
+ {
+ $info['database_fixed_field_settings'] = json_encode( $info['database_fixed_field_settings'] );
+ }
+ }
+ else
+ {
+ $info['database_fixed_field_settings'] = json_encode( array(
+ 'record_image' => array(
+ 'image_dims' => array( 0, 0 ),
+ 'thumb_dims' => array( 200, 200 ),
+ )
+ ) );
+ }
+
+ /* These are things we will not know, or cannot convert (like templates) */
+ $info['database_template_listing'] = 'listing';
+ $info['database_template_display'] = 'display';
+ $info['database_template_categories'] = 'category_index';
+ $info['database_field_title'] = 0; # we will set this later
+ $info['database_field_content'] = 0; # we will set this later
+ $info['database_template_form'] = 'form';
+ $info['database_template_featured'] = 'category_articles';
+
+ /* Forum Integration */
+ try
+ {
+ $this->software->app->checkForSibling( 'forums' );
+ if ( \IPS\Application::appIsEnabled( 'forums' ) === FALSE )
+ {
+ throw new \LogicException;
+ }
+
+ foreach( array( 'database_forum_record', 'database_forum_comments', 'database_forum_delete' ) as $forumZero )
+ {
+ if ( !isset( $info[ $forumZero ] ) )
+ {
+ $info[ $forumZero ] = 0;
+ }
+ }
+
+ foreach( array( 'database_forum_prefix', 'database_forum_suffix' ) as $forumEmpty )
+ {
+ if ( !isset( $info[ $forumEmpty ] ) )
+ {
+ $info[ $forumEmpty ] = '';
+ }
+ }
+
+ if ( isset( $info['database_forum_forum'] ) )
+ {
+ $info['database_forum_forum'] = $this->software->app->getSiblingLink( $info['database_forum_forum'], 'forums_forums', 'forums' );
+ }
+ else
+ {
+ throw new \InvalidArgumentException;
+ }
+ }
+ catch( \OutOfRangeException $e ) # no sibling or forum
+ {
+ $info['database_forum_record'] = 0;
+ $info['database_forum_comments'] = 0;
+ $info['database_forum_delete'] = 0;
+ $info['database_forum_forum'] = 0;
+ $info['database_forum_prefix'] = '';
+ $info['database_forum_suffix'] = '';
+ }
+ catch( \InvalidArgumentException $e ) # no forum provided
+ {
+ $info['database_forum_record'] = 0;
+ $info['database_forum_comments'] = 0;
+ $info['database_forum_delete'] = 0;
+ $info['database_forum_forum'] = 0;
+ $info['database_forum_prefix'] = '';
+ $info['database_forum_suffix'] = '';
+ }
+ catch( \LogicException $e ) # a bug
+ {
+ throw $e;
+ }
+
+ if ( isset( $info['database_tags_predefined'] ) )
+ {
+ if ( is_array( $info['database_tags_predefined'] ) )
+ {
+ $info['database_tags_predefined'] = implode( ',', $info['database_tags_predefined'] );
+ }
+ }
+
+ if ( isset( $info['database_page_id'] ) )
+ {
+ try
+ {
+ $info['database_page_id'] = $this->software->app->getLink( $info['database_page_id'], 'cms_pages' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['database_page_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['database_page_id'] = 0;
+ }
+
+ /* And we're finally done with this table */
+ $id = $info['database_id'];
+ unset( $info['database_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_databases', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_databases' );
+
+ /* And now fields */
+ $validFields = array_merge( static::$fieldTypes, \IPS\cms\Fields::$additionalFieldTypes, array( 'Youtube', 'Spotify', 'Soundcloud' ) );
+ $fieldsInserted = array();
+ $titleField = 0;
+ $contentField = 0;
+ $haveTitleField = FALSE;
+ $haveContentField = FALSE;
+ foreach( $fields AS $field )
+ {
+ if ( !isset( $field['field_id'] ) )
+ {
+ continue;
+ }
+
+ /* We already know this */
+ $field['field_database_id'] = $inserted_id;
+
+ if ( !in_array( $field['field_type'], $validFields ) )
+ {
+ continue;
+ }
+
+ if ( isset( $field['field_name'] ) )
+ {
+ $fieldName = $field['field_name'];
+ unset( $field['field_name'] );
+ }
+ else
+ {
+ $fieldName = ucfirst( $field['field_type'] );
+ }
+
+ if ( isset( $field['field_desc'] ) )
+ {
+ $fieldDesc = $field['field_desc'];
+ unset( $field['field_desc'] );
+ }
+ else
+ {
+ $fieldDesc = '';
+ }
+
+ if ( !isset( $field['field_key'] ) )
+ {
+ $field['field_key'] = \strtolower( $field['field_type'] . '_' . $fieldsInserted );
+ }
+
+ if ( !isset( $field['field_required'] ) )
+ {
+ $field['field_required'] = 0;
+ }
+
+ if ( !isset( $field['field_user_editable'] ) )
+ {
+ $field['field_user_editable'] = 1;
+ }
+
+ if ( !isset( $field['field_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(field_position)', 'cms_database_fields', array( 'field_database_id=?', $inserted_id ) )->first();
+
+ $field['field_position'] = $position + 1;
+ }
+
+ if ( !isset( $field['field_max_length'] ) )
+ {
+ $field['field_max_length'] = 0;
+ }
+
+ if ( !isset( $field['field_extra'] ) )
+ {
+ $field['field_extra'] = '';
+ }
+
+ if ( !isset( $field['field_html'] ) )
+ {
+ $field['field_html'] = 0;
+ }
+
+ if ( !isset( $field['field_truncate'] ) )
+ {
+ $field['field_truncate'] = 100;
+ }
+
+ if ( !isset( $field['field_default_value'] ) )
+ {
+ $field['field_default_value'] = '';
+ }
+
+ if ( !isset( $field['field_display_listing'] ) )
+ {
+ $field['field_display_listing'] = 0;
+ }
+
+ if ( !isset( $field['field_display_display'] ) )
+ {
+ $field['field_display_display'] = 1;
+ }
+
+ if ( isset( $field['field_format_opts'] ) )
+ {
+ $opts = array();
+ if ( !is_array( $field['field_format_opts'] ) )
+ {
+ $field['field_format_opts'] = json_decode( $field['field_format_opts'], TRUE );
+ }
+
+ foreach( $field['field_format_opts'] AS $key => $value )
+ {
+ if ( in_array( $value, array( 'strtolower', 'strtoupper', 'ucfirst', 'ucwords', 'punct', 'numerical', 'bold', 'italic' ) ) )
+ {
+ $opts[$key] = $value;
+ }
+ }
+
+ if ( count( $opts ) )
+ {
+ $field['field_format_opts'] = json_encode( $opts );
+ }
+ else
+ {
+ $field['field_format_opts'] = NULL;
+ }
+ }
+ else
+ {
+ $field['field_format_opts'] = NULL;
+ }
+
+ if ( !isset( $field['field_validator'] ) )
+ {
+ $field['field_validator'] = 0;
+ }
+
+ if ( !isset( $field['field_topic_format'] ) )
+ {
+ $field['field_topic_format'] = '';
+ }
+
+ if ( !isset( $field['field_filter'] ) )
+ {
+ $field['field_filter'] = 0;
+ }
+
+ if ( isset( $field['field_allowed_extensions'] ) )
+ {
+ if ( is_array( $field['field_allowed_extensions'] ) )
+ {
+ $field['field_allowed_extensions'] = json_encode( $field['field_allow_extensions'] );
+ }
+ }
+ else
+ {
+ $field['field_allowed_extensions'] = json_encode( array() );
+ }
+
+ if ( !isset( $field['field_is_multiple'] ) )
+ {
+ $field['field_is_multiple'] = 0;
+ }
+
+ if ( !isset( $field['field_is_searchable'] ) )
+ {
+ $field['field_is_searchable'] = 1;
+ }
+
+ if ( !isset( $field['field_validator_custom'] ) )
+ {
+ $field['field_validator_custom'] = '';
+ }
+
+ if ( !isset( $field['field_display_commentform'] ) )
+ {
+ $field['field_display_commentform'] = 0;
+ }
+
+ if ( isset( $field['field_display_json'] ) )
+ {
+ if ( is_array( $field['field_display_json'] ) )
+ {
+ $field['field_display_json'] = json_encode( $field['field_display_json'] );
+ }
+ }
+ else
+ {
+ $field['field_display_json'] = json_encode( array(
+ 'display' => array( 'method' => NULL ),
+ 'listing' => array( 'method' => NULL )
+ ) );
+ }
+
+ if ( isset( $field['field_is_title'] ) AND $field['field_is_title'] )
+ {
+ $haveTitleField = TRUE;
+ }
+ unset( $field['field_is_title'] );
+
+ if ( isset( $field['field_is_content'] ) AND $field['field_is_content'] )
+ {
+ $haveContentField = TRUE;
+ }
+ unset( $field['field_is_content'] );
+
+ $fieldId = $field['field_id'];
+ unset( $field['field_id'] );
+
+ $fieldInsertedId = \IPS\Db::i()->insert( 'cms_database_fields', $field );
+ \IPS\Lang::saveCustom( 'cms', "content_field_{$fieldInsertedId}", $fieldName );
+ \IPS\Lang::saveCustom( 'cms', "content_field_{$fieldInsertedId}_desc", $fieldDesc );
+ $this->software->app->addLink( $fieldInsertedId, $fieldId, 'cms_database_fields' );
+
+ if ( $haveTitleField AND $titleField == 0 )
+ {
+ $titleField = $fieldInsertedId;
+ }
+
+ if ( $haveContentField AND $contentField == 0 )
+ {
+ $contentField = $fieldInsertedId;
+ }
+
+ $fieldsInserted[$fieldInsertedId] = array( 'type' => $field['field_type'], 'multiple' => $field['field_is_multiple'], 'max_length' => $field['field_max_length'] );
+ }
+
+ if ( count( $fieldsInserted ) > 0 )
+ {
+ \IPS\Db::i()->update( 'cms_databases', array( 'database_field_title' => $titleField, 'database_field_content' => $contentField ), array( "database_id=?", $inserted_id ) );
+ \IPS\Lang::saveCustom( 'cms', "content_db_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'cms', "content_db_{$inserted_id}_desc", $desc );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_sl_{$inserted_id}", $sln );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_pl_{$inserted_id}", $pln );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_su_{$inserted_id}", $scn );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_pu_{$inserted_id}", $pcn );
+ \IPS\Lang::saveCustom( 'cms', "content_db_lang_ia_{$inserted_id}", $ia );
+ }
+ else
+ {
+ /* If we did not insert any fields, delete the database row and log a warning */
+ \IPS\Db::i()->delete( 'cms_databases', array( "database_id=?", $inserted_id ) );
+ $this->software->app->log( 'cms_database_no_fields', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['database_id'] );
+ return FALSE;
+ }
+
+ /* If we're all done, we need to create the table... */
+ $json = json_decode( @file_get_contents( \IPS\ROOT_PATH . "/applications/cms/data/databaseschema.json" ), true );
+ $table = $json['cms_custom_database_1'];
+
+ $table['name'] = 'cms_custom_database_' . $inserted_id;
+
+ foreach( $table['columns'] as $name => $data )
+ {
+ if ( mb_substr( $name, 0, 6 ) === 'field_' )
+ {
+ unset( $table['columns'][ $name ] );
+ }
+ }
+
+ foreach( $table['indexes'] as $name => $data )
+ {
+ if ( mb_substr( $name, 0, 6 ) === 'field_' )
+ {
+ unset( $table['indexes'][ $name ] );
+ }
+ }
+
+ try
+ {
+ if ( ! \IPS\Db::i()->checkForTable( $table['name'] ) )
+ {
+ \IPS\Db::i()->createTable( $table );
+ }
+ }
+ catch( \IPS\Db\Exception $ex )
+ {
+ throw new \LogicException( $ex );
+ }
+
+ foreach( $fieldsInserted as $name => $data )
+ {
+ $columnDefinition = array( 'name' => "field_{$name}" );
+
+ switch( $data['type'] )
+ {
+ case 'CheckboxSet':
+ case 'Member':
+ if ( $data['multiple'] )
+ {
+ $columnDefinition['type'] = 'TEXT';
+ }
+ else
+ {
+ $columnDefinition['type'] = 'INT';
+ $columnDefinition['length'] = 10;
+ }
+ break;
+
+ case 'Date':
+ case 'Poll':
+ $columnDefinition['type'] = 'INT';
+ $columnDefinition['length'] = 10;
+ break;
+
+ case 'Editor':
+ $columnDefinition['type'] = 'MEDIUMTEXT';
+ break;
+
+ case 'TextArea':
+ case 'Upload':
+ case 'Address':
+ case 'Codemirror':
+ case 'Select':
+ case 'Youtube':
+ case 'Spotify':
+ case 'Soundcloud':
+ $columnDefinition['type'] = 'TEXT';
+ break;
+
+ case 'Email':
+ case 'Password':
+ case 'Tel':
+ case 'Text':
+ case 'Url':
+ case 'Color':
+ case 'Radio':
+ case 'Number':
+ $columnDefinition['type'] = 'VARCHAR';
+ $columnDefinition['length'] = 255;
+ break;
+
+ case 'YesNo':
+ case 'Checkbox':
+ case 'Rating':
+ $columnDefinition['type'] = 'TINYINT';
+ $columnDefinition['length'] = 1;
+ break;
+ }
+
+ if ( isset( $data['max_length'] ) AND $data['max_length'] )
+ {
+ $columnDefinition['length'] = $data['max_length'];
+ }
+
+ \IPS\Db::i()->addColumn( "cms_custom_database_{$inserted_id}", $columnDefinition );
+
+ if ( $data['type'] != 'Upload' )
+ {
+ if ( $columnDefinition['type'] == 'TEXT' )
+ {
+ \IPS\Db::i()->addIndex( "cms_custom_database_{$inserted_id}", array( 'type' => 'fulltext', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ else
+ {
+ \IPS\Db::i()->addIndex( "cms_custom_database_{$inserted_id}", array( 'type' => 'key', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ }
+ }
+
+ /* Make sure the page has the appropriate tag */
+ try
+ {
+ $page = \IPS\Db::i()->select( 'page_content', 'cms_pages', array( "page_id=?", $info['database_page_id'] ) )->first();
+
+ if ( preg_match( '/\{database=\"' . $id . '\"\}/', $page ) )
+ {
+ $page = str_replace( "{database=\"{$id}\"}", "{database=\"{$inserted_id}\"}", $page );
+ }
+ else
+ {
+ $page .= "\n{database=\"{$inserted_id}\"}";
+ }
+
+ \IPS\Db::i()->update( 'cms_pages', array( 'page_content' => $page ), array( "page_id=?", $info['database_page_id'] ) );
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \IPS\Db\Exception $e ) {}
+
+ unset( \IPS\Data\Store::i()->cms_databases );
+
+ /* And return */
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a database category
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted category, or FALSE on failure.
+ */
+ public function convertCmsDatabaseCategory( $info=array() )
+ {
+ if ( !isset( $info['category_id'] ) )
+ {
+ $this->software->app->log( 'cms_database_category_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['category_database_id'] ) )
+ {
+ try
+ {
+ $info['category_database_id'] = $this->software->app->getLink( $info['category_database_id'], 'cms_databases' );
+ }
+ catch( \OutRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_category_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['category_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_category_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['category_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['category_name'] ) )
+ {
+ $info['category_name'] = \IPS\Member::loggedIn()->language()->get( "content_db_{$info['category_database_id']}" ) . " Category {$info['category_id']}";
+ }
+
+ if ( array_key_exists( 'category_desc', $info ) )
+ {
+ $desc = $info['category_desc'];
+ unset( $info['category_desc'] );
+ }
+ else
+ {
+ $desc = '';
+ }
+
+ /* Do not know */
+ $info['category_last_record_id'] = 0;
+
+ if ( isset( $info['category_last_record_date'] ) )
+ {
+ if ( $info['category_last_record_date'] instanceof \IPS\DateTime )
+ {
+ $info['category_last_record_date'] = $info['category_last_record_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['category_last_record_date'] = 0;
+ }
+
+ if ( isset( $info['category_last_record_member'] ) )
+ {
+ try
+ {
+ $info['category_last_record_member'] = $this->software->app->getLink( $info['category_last_record_member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['category_last_record_member'] = 0;
+ }
+ }
+ else
+ {
+ $info['category_last_record_member'] = 0;
+ }
+
+ if ( !isset( $info['category_last_record_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['category_last_record_member'] );
+
+ if ( $author->member_id )
+ {
+ $info['category_last_record_name'] = $author->name;
+ }
+ else
+ {
+ $info['category_last_record_name'] = NULL;
+ }
+ }
+ else
+ {
+ $info['category_last_record_name'] = NULL;
+ }
+
+ if ( !isset( $info['category_last_record_seo_name'] ) )
+ {
+ if ( !is_null( $info['category_last_record_name'] ) )
+ {
+ $info['category_last_record_seo_name'] = \IPS\Http\Url::seoTitle( $info['category_last_record_name'] );
+ }
+ else
+ {
+ $info['category_last_record_seo_name'] = NULL;
+ }
+ }
+ else
+ {
+ $info['category_last_record_seo_name'] = NULL;
+ }
+
+ if ( !isset( $info['category_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(category_position)', 'cms_database_categories', array( "category_database_id=?", $info['category_database_id'] ) )->first();
+
+ $info['category_position'] = $position + 1;
+ }
+
+ /* Counts and other zero defaults to make my life easier */
+ foreach( array( 'category_records', 'category_records_queued', 'category_record_comments', 'category_record_comments_queued', 'category_has_perms', 'category_allow_rating', 'category_records_future', 'category_record_reviews', 'category_record_reviews_queued' ) AS $zero )
+ {
+ if ( !isset( $info[ $zero ] ) )
+ {
+ $info[ $zero ] = 0;
+ }
+ }
+
+ if ( !isset( $info['category_show_records'] ) )
+ {
+ $info['category_show_records'] = 1;
+ }
+
+ if ( !isset( $info['category_furl_name'] ) )
+ {
+ $info['category_furl_name'] = \IPS\Http\Url::seoTitle( $info['category_name'] );
+ }
+
+ if ( isset( $info['category_meta_keywords'] ) )
+ {
+ if ( is_array( $info['category_meta_keywords'] ) )
+ {
+ $info['category_meta_keywords'] = implode( ',', $info['category_meta_keywords'] );
+ }
+ }
+ else
+ {
+ $info['category_meta_keywords'] = NULL;
+ }
+
+ if ( !isset( $info['category_meta_description'] ) )
+ {
+ $info['category_meta_description'] = NULL;
+ }
+
+ /* Forum Integration */
+ try
+ {
+ $this->software->app->checkForSibling( 'forums' );
+ if ( \IPS\Application::appIsEnabled( 'forums' ) === FALSE )
+ {
+ throw new \LogicException;
+ }
+
+ foreach( array( 'category_forum_override', 'category_forum_record', 'category_forum_comments', 'category_forum_delete' ) AS $forumZero )
+ {
+ if ( !isset( $info[ $forumZero ] ) )
+ {
+ $info[ $forumZero ] = 0;
+ }
+ }
+
+ if ( isset( $info['category_forum_forum'] ) )
+ {
+ $info['category_forum_forum'] = $this->software->app->getSiblingLink( $info['category_forum_forum'], 'forums_forums', 'forums' );
+ }
+ else
+ {
+ throw new \InvalidArgumentException;
+ }
+
+ foreach( array( 'category_forum_prefix', 'category_forum_suffix' ) as $forumNull )
+ {
+ if ( !isset( $info[ $forumNull ] ) )
+ {
+ $info[ $forumNull ] = NULL;
+ }
+ }
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['category_forum_override'] = 0;
+ $info['category_forum_record'] = 0;
+ $info['category_forum_comments'] = 0;
+ $info['category_forum_delete'] = 0;
+ $info['category_forum_forum'] = 0;
+ $info['category_forum_prefix'] = NULL;
+ $info['category_forum_suffix'] = NULL;
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ $info['category_forum_override'] = 0;
+ $info['category_forum_record'] = 0;
+ $info['category_forum_comments'] = 0;
+ $info['category_forum_delete'] = 0;
+ $info['category_forum_forum'] = 0;
+ $info['category_forum_prefix'] = NULL;
+ $info['category_forum_suffix'] = NULL;
+ }
+ catch( \LogicException $e )
+ {
+ throw $e;
+ }
+
+ if ( !isset( $info['category_full_path'] ) )
+ {
+ $info['category_full_path'] = \IPS\Http\Url::seoTitle( $info['category_name'] );
+ }
+
+ if ( !isset( $info['category_last_title'] ) )
+ {
+ $info['category_last_title'] = NULL;
+ }
+
+ if ( !isset( $info['category_last_seo_title'] ) )
+ {
+ if ( !is_null( $info['category_last_title'] ) )
+ {
+ $info['category_last_seo_title'] = \IPS\Http\Url::seoTitle( $info['category_last_title'] );
+ }
+ else
+ {
+ $info['category_last_seo_title'] = NULL;
+ }
+ }
+ else
+ {
+ $info['category_last_seo_title'] = NULL;
+ }
+
+ if ( isset( $info['category_fields'] ) )
+ {
+ if ( !is_array( $info['category_fields'] ) )
+ {
+ $info['category_fields'] = json_decode( $info['category_fields'], TRUE );
+ }
+
+ $fields = array();
+ foreach( $info['category_fields'] AS $field )
+ {
+ try
+ {
+ $fields[] = $this->software->app->getLink( $field, 'cms_database_fields' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+
+ if ( count( $fields ) )
+ {
+ $info['category_fields'] = json_encode( $fields );
+ }
+ else
+ {
+ $info['category_fields'] = NULL;
+ }
+ }
+ else
+ {
+ $info['category_fields'] = NULL;
+ }
+
+ /* Not Used? I'm confused. */
+ $info['category_options'] = 0;
+
+ /* Do not know */
+ $info['category_template_listing'] = 0;
+ $info['category_template_display'] = 0;
+
+ $id = $info['category_id'];
+ unset( $info['category_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_database_categories', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_database_categories' );
+
+ \IPS\Lang::saveCustom( 'cms', "content_cat_name_{$inserted_id}", $info['category_name'] );
+ \IPS\Lang::saveCustom( 'cms', "content_cat_name_{$inserted_id}_desc", $desc );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Record
+ *
+ * @param array $info The data to insert.
+ * @param array $fields Custom Field Data.
+ * @return integer|boolean The ID of the newly inserted record, or FALSE on failure.
+ */
+ public function convertCmsDatabaseRecord( $info=array(), $fields=array(), $imagepath=NULL, $imagedata=NULL )
+ {
+ if ( !isset( $info['record_id'] ) )
+ {
+ $this->software->app->log( 'cms_database_record_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['record_database_id'] ) )
+ {
+ try
+ {
+ $database = $this->software->app->getLink( $info['record_database_id'], 'cms_databases' );
+ unset( $info['record_database_id'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_record_missing_datebase', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['record_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_record_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['record_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['member_id'] ) ) # inconsistency ftl
+ {
+ try
+ {
+ $info['member_id'] = $this->software->app->getLink( $info['member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['member_id'] = 0;
+ }
+
+ /* We can knock out a lot of this right now */
+ foreach( array( 'post_key', 'rating_real', 'rating_hits', 'rating_value', 'record_locked', 'record_comments', 'record_comments_queued', 'record_comments_hidden', 'record_views', 'record_pinned', 'record_template', 'record_on_homepage', 'record_allow_comments', 'record_allow_reviews', 'record_featured', 'record_reviews', 'record_reviews_queued', 'record_rating', 'record_edit_show' ) AS $zero )
+ {
+ if ( !isset( $info[ $zero ] ) )
+ {
+ $info[ $zero ] = 0;
+ }
+ }
+
+ /* And these too */
+ foreach( array( 'record_saved', 'record_updated', 'record_future_date', 'record_expiry_date', 'record_comment_cutoff', 'record_edit_time' ) AS $date )
+ {
+ if ( isset( $info[ $date ] ) )
+ {
+ if ( $info[ $date ] instanceof \IPS\DateTime )
+ {
+ $info[ $date ] = $info[ $date ]->getTimestamp();
+ }
+ }
+ else
+ {
+ $info[ $date ] = 0;
+ }
+ }
+
+ if ( isset( $info['category_id'] ) ) # sigh
+ {
+ try
+ {
+ $info['category_id'] = $this->software->app->getLink( $info['category_id'], 'cms_database_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_record_missing_category', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['record_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_record_missing_category', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['record_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['record_approved'] ) )
+ {
+ $info['record_approved'] = 1;
+ }
+
+ $fields = $this->_formatFieldsForSaving( $fields );
+
+ if ( !isset( $info['record_dynamic_furl'] ) )
+ {
+ $titleField = \IPS\Db::i()->select( 'database_field_title', 'cms_databases', array( "database_id=?", $database ) )->first();
+
+ $info['record_dynamic_furl'] = \IPS\Http\Url::seoTitle( $fields['field_' . $titleField] );
+ }
+
+ if ( !isset( $info['record_static_furl'] ) )
+ {
+ $info['record_static_furl'] = NULL;
+ }
+
+ if ( isset( $info['record_meta_keywords'] ) )
+ {
+ if ( is_array( $info['record_meta_keywords'] ) )
+ {
+ $info['record_meta_keywords'] = implode( ',', $info['record_meta_keywords'] );
+ }
+ }
+ else
+ {
+ $info['record_meta_keywords'] = NULL;
+ }
+
+ if ( !isset( $info['record_meta_description'] ) )
+ {
+ $info['record_meta_description'] = NULL;
+ }
+
+ if ( isset( $info['record_topicid'] ) )
+ {
+ try
+ {
+ $this->software->app->checkForSibling( 'forums' );
+ if ( \IPS\Application::appIsEnabled( 'forums' ) === FALSE )
+ {
+ throw new \LogicException;
+ }
+
+ $info['record_topicid'] = $this->software->app->getSiblingLink( $info['record_topicid'], 'forums_topics', 'forums' );
+ }
+ catch( \OutOfrangeException $e )
+ {
+ $info['record_topicid'] = 0;
+ }
+ catch( \LogicException $e )
+ {
+ throw $e;
+ }
+ }
+ else
+ {
+ $info['record_topicid'] = 0;
+ }
+
+ foreach( array( 'record_publish_date', 'record_last_comment', 'record_last_review' ) AS $currentDate )
+ {
+ if ( isset( $info[ $currentDate ] ) )
+ {
+ if ( $info[ $currentDate ] instanceof \IPS\DateTime )
+ {
+ $info[ $currentDate ] = $info[ $currentDate ]->getTimestamp();
+ }
+
+ /* Last comment and last review dates cannot be in the future */
+ if( in_array( $currentDate, array( 'record_last_comment', 'record_last_review' ) ) AND $info[ $currentDate ] > time() )
+ {
+ $info[ $currentDate ] = time();
+ }
+ }
+ else
+ {
+ $info[ $currentDate ] = time();
+ }
+ }
+
+ if ( isset( $info['record_last_comment_by'] ) )
+ {
+ try
+ {
+ $info['record_last_comment_by'] = $this->software->app->getLink( $info['record_last_comment_by'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['record_last_comment_by'] = 0;
+ }
+ }
+ else
+ {
+ $info['record_last_comment_by'] = 0;
+ }
+
+ if ( !isset( $info['record_last_comment_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['record_last_comment_by'] );
+
+ if ( $author->member_id )
+ {
+ $info['record_last_comment_name'] = $author->name;
+ }
+ else
+ {
+ $info['record_last_comment_name'] = NULL;
+ }
+ }
+ else
+ {
+ $info['record_last_comment_name'] = NULL;
+ }
+
+ if ( isset( $info['record_image'] ) AND ( !is_null( $imagepath ) OR !is_null( $imagedata ) ) )
+ {
+ try
+ {
+ if ( is_null( $imagedata ) AND !is_null( $imagepath ) )
+ {
+ $imagedata = @file_get_contents( $imagepath );
+ $imagepath = NULL;
+ }
+
+ $file = \IPS\File::create( 'cms_Records', $info['record_image'], $imagedata );
+ $info['record_image'] = (string) $file;
+ $info['record_image_thumb'] = (string) $file->thumbnail();
+ }
+ catch( \Exception $e )
+ {
+ $info['record_image'] = NULL;
+ $info['record_image_thumb'] = NULL;
+ }
+ }
+ else
+ {
+ $info['record_image'] = NULL;
+ $info['record_image_thumb'] = NULL;
+ }
+
+ if ( isset( $info['record_last_review_by'] ) )
+ {
+ try
+ {
+ $info['record_last_review_by'] = $this->software->app->getLink( $info['record_last_review_by'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['record_last_review_by'] = 0;
+ }
+ }
+ else
+ {
+ $info['record_last_review_by'] = 0;
+ }
+
+ if ( !isset( $info['record_last_review_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['record_last_review_by'] );
+
+ if ( $author->member_id )
+ {
+ $info['record_last_review_name'] = $author->name;
+ }
+ else
+ {
+ $info['record_last_review_name'] = NULL;
+ }
+ }
+ else
+ {
+ $info['record_last_review_name'] = NULL;
+ }
+
+ if ( !isset( $info['record_edit_reason'] ) )
+ {
+ $info['record_edit_reason'] = NULL;
+ }
+
+ if ( isset( $info['record_edit_member_id'] ) )
+ {
+ try
+ {
+ $info['record_edit_member_id'] = $this->software->app->getLink( $info['record_edit_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['record_edit_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['record_edit_member_id'] = 0;
+ }
+
+ if ( !isset( $info['record_edit_member_name'] ) )
+ {
+ $member = \IPS\Member::load( $info['record_edit_member_id'] );
+
+ if ( $member->member_id )
+ {
+ $info['record_edit_member_name'] = $member->name;
+ }
+ else
+ {
+ $info['record_edit_member_name'] = NULL;
+ }
+ }
+
+ /* Merge in custom field data */
+ $info = array_merge( $info, $fields );
+
+ $id = $info['record_id'];
+ unset( $info['record_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( "cms_custom_database_{$database}", $info );
+ $this->software->app->addLink( $inserted_id, $id, "cms_custom_database_{$database}" );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Format Field Content
+ *
+ * @param array $fieldInfo The Field Information to format. This SHOULD be in $foreign_id => $content format, however field_$foreign_id => $content is also accepted.
+ * @return array An array of data formatted for cms_custom_database_*
+ */
+ protected function _formatFieldsForSaving( $fieldInfo )
+ {
+ $return = array();
+
+ if ( count( $fieldInfo ) )
+ {
+ foreach( $fieldInfo as $key => $value )
+ {
+ if ( preg_match( '/^field_(\d+)/i', $key, $matches ) )
+ {
+ $id = str_replace( 'field_', '', $matches[1] );
+ }
+ else if ( is_numeric( $key ) )
+ {
+ $id = $key;
+ }
+ else
+ {
+ continue;
+ }
+
+ try
+ {
+ $link = $this->software->app->getLink( $id, 'cms_database_fields' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Does not exist - skip */
+ continue;
+ }
+
+ $return[ 'field_' . $link ] = $value;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Convert a Comment
+ *
+ * @param array $info Data to insert.
+ * @return integer|boolean The ID of the newly inserted comment, or FALSE on failure.
+ */
+ public function convertCmsDatabaseComment( $info=array() )
+ {
+ if ( !isset( $info['comment_id'] ) )
+ {
+ $this->software->app->log( 'cms_database_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_user'] ) )
+ {
+ try
+ {
+ $info['comment_user'] = $this->software->app->getLink( $info['comment_user'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_user'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_user'] = 0;
+ }
+
+ if ( isset( $info['comment_database_id'] ) )
+ {
+ try
+ {
+ $info['comment_database_id'] = $this->software->app->getLink( $info['comment_database_id'], 'cms_databases' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_comment_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_comment_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_record_id'] ) )
+ {
+ try
+ {
+ $info['comment_record_id'] = $this->software->app->getLink( $info['comment_record_id'], "cms_custom_database_{$info['comment_database_id']}" );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_comment_missing_record', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_comment_missing_record', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_date'] ) )
+ {
+ if ( $info['comment_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_date'] = $info['comment_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_date'] = time();
+ }
+
+ if ( !isset( $info['comment_ip_address'] ) OR filter_var( $info['comment_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['comment_ip_address'] = '127.0.0.1';
+ }
+
+ if ( empty( $info['comment_post'] ) )
+ {
+ $this->software->app->log( 'cms_database_comment_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['comment_approved'] ) )
+ {
+ $info['comment_approved'] = 1;
+ }
+
+ if ( !isset( $info['comment_author'] ) )
+ {
+ $author = \IPS\Member::load( $info['comment_user'] );
+
+ if ( $author->member_id )
+ {
+ $info['comment_author'] = $author->name;
+ }
+ else
+ {
+ $info['comment_author'] = NULL;
+ }
+ }
+ else
+ {
+ $info['comment_author'] = NULL;
+ }
+
+ if ( isset( $info['comment_edit_date'] ) )
+ {
+ if ( $info['comment_edit_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_edit_date'] = $info['comment_edit_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_edit_date'] = 0;
+ }
+
+ if ( !isset( $info['comment_edit_reason'] ) )
+ {
+ $info['comment_edit_reason'] = NULL;
+ }
+
+ if ( isset( $info['comment_edit_member_id'] ) )
+ {
+ try
+ {
+ $info['comment_edit_member_id'] = $this->software->app->getLink( $info['comment_edit_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_edit_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_edit_member_id'] = 0;
+ }
+
+ if ( !isset( $info['comment_edit_member_name'] ) )
+ {
+ $member = \IPS\Member::load( $info['comment_edit_member_id'] );
+
+ if ( $member->member_id )
+ {
+ $info['comment_edit_member_name'] = $member->name;
+ }
+ else
+ {
+ $info['comment_edit_member_name'] = NULL;
+ }
+ }
+ else
+ {
+ $info['comment_edit_member_name'] = NULL;
+ }
+
+ if ( !isset( $info['comment_edit_show'] ) )
+ {
+ $info['comment_edit_show'] = 0;
+ }
+
+ $id = $info['comment_id'];
+ unset( $info['comment_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_database_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_database_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a review
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted review, or FALSE on failure.
+ */
+ public function convertCmsDatabaseReview( $info=array() )
+ {
+ if ( !isset( $info['review_id'] ) )
+ {
+ $this->software->app->log( 'cms_database_review_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_database_id'] ) )
+ {
+ try
+ {
+ $info['review_database_id'] = $this->software->app->getLink( $info['review_database_id'], 'cms_databases' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_review_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_review_missing_database', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_item'] ) )
+ {
+ try
+ {
+ $info['review_item'] = $this->software->app->getLink( $info['review_item'], "cms_custom_database_{$info['review_database_id']}" );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_review_missing_item', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_review_missing_item', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ /* Unlike comments, guests cannot review */
+ if ( isset( $info['review_author'] ) )
+ {
+ try
+ {
+ $info['review_author'] = $this->software->app->getLink( $info['review_author'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'cms_database_review_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'cms_database_review_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['review_content'] ) )
+ {
+ $this->software->app->log( 'cms_database_review_empty', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ /* This seems silly, but we really do need a rating */
+ if ( !isset( $info['review_rating'] ) OR $info['review_rating'] < 1 )
+ {
+ $this->software->app->log( 'calendar_event_review_invalid_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['review_edit_show'] ) )
+ {
+ $info['review_edit_show'] = 0;
+ }
+
+ if ( isset( $info['review_edit_time'] ) )
+ {
+ if ( $info['review_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['review_edit_time'] = $info['review_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_edit_time'] = time();
+ }
+
+ if ( !isset( $info['review_edit_name'] ) )
+ {
+ $info['review_edit_name'] = NULL;
+ }
+
+ if ( isset( $info['review_date'] ) )
+ {
+ if ( $info['review_date'] instanceof \IPS\DateTime )
+ {
+ $info['review_date'] = $info['review_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_date'] = time();
+ }
+
+ if ( !isset( $info['review_ip_address'] ) OR filter_var( $info['review_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['review_ip_address'] = '127.0.0.1';
+ }
+
+ if ( !isset( $info['review_author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['review_mid'] );
+
+ if ( $author->member_id )
+ {
+ $info['review_author_name'] = $author->name;
+ }
+ else
+ {
+ $info['review_author_name'] = "Guest";
+ }
+ }
+
+ if ( isset( $info['review_votes_data'] ) )
+ {
+ if ( !is_array( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_data'] = json_decode( $info['review_votes_data'], TRUE );
+ }
+
+ $newVoters = array();
+ if ( !is_null( $info['review_votes_data'] ) AND count( $info['review_votes_data'] ) )
+ {
+ foreach( $info['review_votes_data'] AS $member => $vote )
+ {
+ try
+ {
+ $memberId = $this->software->app->getLink( $member, 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ $newVoters[ $memberId ] = $vote;
+ }
+ }
+
+ if ( count( $newVoters ) )
+ {
+ $info['review_votes_data'] = json_encode( $newVoters );
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+
+ if ( !isset( $info['review_votes_total'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_total'] = 0;
+ }
+ else
+ {
+ $info['review_votes_total'] = count( json_decode( $info['review_votes_data'], TRUE ) );
+ }
+ }
+
+ if ( !isset( $info['review_votes_helpful'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_helpful'] = 0;
+ }
+ else
+ {
+ $helpful = 0;
+ foreach( json_decode( $info['review_votes_data'], TRUE ) AS $member => $vote )
+ {
+ if ( $vote == 1 )
+ {
+ $helpful += 1;
+ }
+ }
+
+ $info['review_votes_helpful'] = $helpful;
+ }
+ }
+
+ if ( !isset( $info['review_approved'] ) )
+ {
+ $info['review_approved'] = 1;
+ }
+
+ $id = $info['review_id'];
+ unset( $info['review_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_database_reviews', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_database_reviews' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Media Folder
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted folder, or FALSE on failure.
+ */
+ public function convertCmsMediaFolder( $info=array() )
+ {
+ if ( !isset( $info['media_folder_id'] ) )
+ {
+ $this->software->app->log( 'cms_media_folder_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['media_folder_name'] ) )
+ {
+ $info['media_folder_name'] = "untitledfolder{$info['media_folder_id']}";
+ }
+
+ if ( isset( $info['media_folder_parent'] ) )
+ {
+ try
+ {
+ $info['media_folder_parent'] = $this->software->app->getLink( $info['media_folder_parent'], 'cms_media_folders' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['media_conv_parent'] = $info['media_folder_parent'];
+ }
+ }
+ else
+ {
+ $info['media_folder_parent'] = 0;
+ }
+
+ if ( !isset( $info['media_folder_path'] ) )
+ {
+ $info['member_folder_path'] = $info['media_folder_name'];
+ }
+
+ $id = $info['media_folder_id'];
+ unset( $info['media_folder_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_media_folders', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_media_folders' );
+
+ \IPS\Db::i()->update( 'cms_media_folders', array( "media_folder_parent" => $inserted_id ), array( "media_conv_parent=? AND media_conv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a media file
+ *
+ * @param array $info Data to insert
+ * @param string|NULL $filepath The path to the file, or NULL.
+ * @param string|NULL $filedata The data for the file, or NULL.
+ * @return integer|boolean The ID of the newly inserted media file, or FALSE on failure.
+ */
+ public function convertCmsMedia( $info=array(), $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['media_id'] ) )
+ {
+ $this->software->app->log( 'cms_media_file_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( is_null( $filepath ) AND is_null( $filedata ) )
+ {
+ $this->software->app->log( 'cms_media_file_no_file', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['media_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['media_parent'] ) )
+ {
+ try
+ {
+ $info['media_parent'] = $this->software->app->getLink( $info['media_parent'], 'cms_media_folders' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['media_parent'] = 0;
+ }
+ }
+ else
+ {
+ $info['media_parent'] = 0;
+ }
+
+ if ( !isset( $info['media_filename'] ) )
+ {
+ if ( !is_null( $filepath ) )
+ {
+ $name = explode( '/', $filepath );
+ $name = array_pop( $name );
+ $info['media_filename'] = $name;
+ }
+ else
+ {
+ $this->software->app->log( 'cms_media_file_missing_name', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['media_id'] );
+ return FALSE;
+ }
+ }
+
+ if ( isset( $info['media_added'] ) )
+ {
+ if ( $info['media_added'] instanceof \IPS\DateTime )
+ {
+ $info['media_added'] = $info['media_added']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['media_added'] = time();
+ }
+
+ if ( !isset( $info['media_full_path'] ) )
+ {
+ try
+ {
+ $folderPath = \IPS\Db::i()->select( 'media_folder_path', 'cms_media_folders', array( "media_folder_id=?", $info['media_parent'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $folderPath = '';
+ }
+
+ $info['media_full_path'] = ltrim( $folderPath . '/' . $info['media_filename'] );
+ }
+
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = @file_get_contents( $filepath );
+ $filepath = NULL;
+ }
+
+ $file = \IPS\File::create( 'cms_Media', $info['media_parent'] . '_' . $info['media_filename'], $filedata, 'pages_media' );
+ $info['media_file_object'] = (string) $file;
+ $info['media_filename_stored'] = $info['media_parent'] . '_' . $info['media_filename'];
+
+ if ( !isset( $info['media_is_image'] ) )
+ {
+ if ( $file->isImage() )
+ {
+ $info['media_is_image'] = 1;
+ }
+ else
+ {
+ $info['media_is_image'] = 0;
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['media_id'] );
+ return FALSE;
+ }
+ catch( \ErrorException $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['media_id'] );
+ return FALSE;
+ }
+
+ $id = $info['media_id'];
+ unset( $info['media_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'cms_media', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'cms_media' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Attachment Map Data
+ * @param string|NULL $filepath Path to the file, or NULL.
+ * @param string|NULL $filedata Binary data for the file, or NULL.
+ * @return integer|boolean The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ $database = $this->software->app->getLink( str_replace( '-review', '', $map['id3'] ), 'cms_databases' );
+
+ $map['id1_type'] = "cms_custom_database_{$database}";
+ $map['id1_from_parent'] = FALSE;
+ $map['id2_from_parent'] = FALSE;
+ $map['id3_skip_link'] = TRUE;
+ $map['location_key'] = "cms_Records{$database}";
+
+ if ( \substr( $map['id3'], -1, 7 ) == '-review' )
+ {
+ $map['id2_type'] = 'cms_database_reviews';
+ }
+ else
+ {
+ $map['id2_type'] = 'cms_database_comments';
+ }
+
+ $map['id3'] = $database;
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Core.php b/applications/convert/sources/Library/Core.php
new file mode 100644
index 00000000..d006cd75
--- /dev/null
+++ b/applications/convert/sources/Library/Core.php
@@ -0,0 +1,5434 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Core extends \IPS\convert\Library
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'core';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertAcronyms':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_acronyms',
+ 'step_method' => 'convertAcronyms',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_acronyms' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array(),
+ 'link_type' => 'core_acronyms',
+ );
+ break;
+
+ case 'convertAdministrators':
+ $dependencies = arrau();
+
+ if ( array_key_exists( 'convertGroups', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGroups';
+ }
+
+ $dependencies[] = 'convertMembers';
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_administrators',
+ 'step_method' => 'convertAdministrators',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_admin_permission_rows' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'core_administrators',
+ );
+ break;
+
+ case 'convertAnnouncements':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_announcements',
+ 'step_method' => 'convertAnnouncements',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_announcements' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_announcements',
+ );
+ break;
+
+ case 'convertAttachments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_attachments',
+ 'step_method' => 'convertAttachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+
+ case 'convertBanfilters':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_banfilters',
+ 'step_method' => 'convertBanfilters',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_banfilters' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_banfilters',
+ );
+ break;
+
+ case 'convertCustomBbcode':
+ $return[$k] = array(
+ 'step_title' => 'convert_custom_bbcode',
+ 'step_method' => 'convertCustomBbcode',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'custom_bbcode' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array(),
+ 'link_type' => 'core_bbcode',
+ );
+ break;
+
+ case 'convertDnameChanges':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_dname_changes',
+ 'step_method' => 'convertDnameChanges',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_dnames_change' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_dname_changes',
+ );
+ break;
+
+ case 'convertEditHistory':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_edit_history',
+ 'step_method' => 'convertEditHistory',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_edit_history' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_edit_history',
+ );
+ break;
+
+ case 'convertEmoticons':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_emoticons',
+ 'step_method' => 'convertEmoticons',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_emoticons' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_emoticons',
+ );
+ break;
+
+ case 'convertGroups':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_groups',
+ 'step_method' => 'convertGroups',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_groups' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_groups',
+ );
+ break;
+
+ case 'convertIgnoredUsers':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_ignored_users',
+ 'step_method' => 'convertIgnoredUsers',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_ignored_users' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_ignored_users',
+ );
+ break;
+
+ case 'convertLeaderGroups':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_leader_groups',
+ 'step_method' => 'convertLeaderGroups',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_leaders_groups' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_leader_groups',
+ );
+ break;
+
+ case 'convertLeaders':
+ /* Dependencies change based on what we can convert */
+ $dependencies = array();
+
+ if ( array_key_exists( 'convertLeaderGroups', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertLeaderGroups';
+ }
+
+ if ( array_key_exists( 'convertGroups', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGroups';
+ }
+
+ $dependencies[] = 'convertMembers';
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_leaders',
+ 'step_method' => 'convertLeaders',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_leaders' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'core_leaders',
+ );
+ break;
+
+ case 'convertRanks':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_ranks',
+ 'step_method' => 'convertRanks',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_member_ranks' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_member_ranks',
+ );
+ break;
+
+ case 'convertStatuses':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_statuses',
+ 'step_method' => 'convertStatuses',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_member_status_updates' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_member_status_updates',
+ );
+ break;
+
+ case 'convertStatusReplies':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_status_replies',
+ 'step_method' => 'convertStatusReplies',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_member_status_replies' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertMembers', 'convertStatuses' ),
+ 'link_type' => 'core_member_status_replies',
+ );
+ break;
+
+ case 'convertMembers':
+ $dependencies = array();
+
+ if ( array_key_exists( 'convertGroups', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGroups';
+ }
+
+ if ( array_key_exists( 'convertProfileFields', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertProfileFields';
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_members',
+ 'step_method' => 'convertMembers',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 250,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'core_members',
+ );
+ break;
+
+ case 'convertWarnActions':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_warn_actions',
+ 'step_method' => 'convertWarnActions',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members_warn_actions' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_members_warn_actions',
+ );
+ break;
+
+ case 'convertWarnReasons':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_warn_reasons',
+ 'step_method' => 'convertWarnReasons',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_members_warn_reasons' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_members_warn_reasons',
+ );
+ break;
+
+ case 'convertPrivateMessages':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_private_messages',
+ 'step_method' => 'convertPrivateMessages',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_message_topics' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => array( 'core_message_topics', 'core_message_topic_user_map' ),
+ );
+ break;
+
+ case 'convertPrivateMessageReplies':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_private_message_replies',
+ 'step_method' => 'convertPrivateMessageReplies',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_message_posts' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertPrivateMessages' ),
+ 'link_type' => array( 'core_message_posts' )
+ );
+ break;
+
+ case 'convertModerators':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_moderators',
+ 'step_method' => 'convertModerators',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_moderators' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertMembers' ),
+ 'link_type' => 'core_members',
+ );
+ break;
+
+ case 'convertProfileFieldGroups':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_profile_field_groups',
+ 'step_method' => 'convertProfileFieldGroups',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_pfields_groups' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'core_pfields_groups',
+ );
+ break;
+
+ case 'convertProfileFields':
+ $dependencies = array();
+
+ if ( array_key_exists( 'convertProfileFieldGroups', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertProfileFieldGroups';
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_profile_fields',
+ 'step_method' => 'convertProfileFields',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_pfields_data' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'core_pfields_data',
+ );
+ break;
+
+ case 'convertProfanityFilters':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_profanity_filters',
+ 'step_method' => 'convertProfanityFilters',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_profanity_filters' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array(),
+ 'link_type' => 'core_profanity_filters',
+ );
+ break;
+
+ case 'convertQuestionAndAnswers':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_question_and_answers',
+ 'step_method' => 'convertQuestionAndAnswers',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_question_and_answer' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array(),
+ 'link_type' => 'core_question_and_answer',
+ );
+ break;
+
+ case 'convertReportComments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_report_comments',
+ 'step_method' => 'convertReportComments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_rc_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array(),
+ 'link_type' => 'core_report_comments',
+ );
+ break;
+
+ case 'convertReputationLevels':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_reputation_levels',
+ 'step_method' => 'convertReputationLevels',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_reputation_levels' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array(),
+ 'link_type' => 'core_reputation_levels',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @param string The method to truncate
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ /* Should return multiple array members for each table that needs to be truncated. The key should be the table, while the value should be a WHERE clause, or NULL to completely empty the table */
+ case 'convertAcronyms':
+ $return['convertAcronyms'] = array( 'core_acronyms' => NULL );
+ break;
+
+ case 'convertAdministrators':
+ $return['convertAdministrators'] = array( 'core_administrators', array( "( row_id!=? AND row_id_type!=? ) OR ( row_id!=? AND row_id_type!=?", \IPS\Member::loggedIn()->member_id, 'member', \IPS\Member::i()->loggedIn()->member_group_id, 'group' ) );
+ break;
+
+ case 'convertAnnouncements':
+ $return['convertAnnouncements'] = array( 'core_announcements' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $return['convertAttachments'] = array( 'core_attachments' => NULL, 'core_attachments_map' => NULL );
+ break;
+
+ case 'convertBanfilters':
+ $return['convertBanfilters'] = array( 'core_banfilters' => NULL );
+ break;
+
+ case 'convertCustomBbcode':
+ $return['convertCustomBbcode'] = array( 'convert_custom_bbcode' => NULL );
+ break;
+
+ case 'convertDnameChanges':
+ $return['convertDnameChanges'] = array( 'core_dnames_change' => NULL );
+ break;
+
+ case 'convertEditHistory':
+ $return['convertEditHistory'] = array( 'core_edit_history' => NULL );
+ break;
+
+ case 'convertEmoticons':
+ $return['convertEmoticons'] = array( 'core_emoticons' => NULL );
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array( 'core_groups' => array( \IPS\Db::i()->in( 'g_id', array( \IPS\Settings::i()->member_group, \IPS\Settings::i()->guest_group, \IPS\Settings::i()->admin_group ), TRUE ) ) );
+ break;
+
+ case 'convertIgnoredUsers':
+ $return['convertIgnoredUsers'] = array( 'core_ignored_users' => NULL );
+ break;
+
+ case 'convertLeaders':
+ $return['convertLeaders'] = array( 'core_leaders' => NULL );
+ break;
+
+ case 'convertLeaderGroups':
+ $return['convertLeaderGroups'] = array( 'core_leaders_groups' => NULL );
+ break;
+
+ case 'convertRanks':
+ $return['convertRanks'] = array( 'core_member_ranks' => NULL );
+ break;
+
+ case 'convertStatusReplies':
+ $return['convertStatusReplies'] = array( 'core_member_status_replies' => NULL );
+ break;
+
+ case 'convertStatusUpdates':
+ $return['convertStatusUpdates'] = array( 'core_member_status_updates' => NULL );
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array( 'core_members' => array( "member_id<>?", \IPS\Member::loggedIn()->member_id ), 'core_pfields_content' => array( "member_id<>?", \IPS\Member::loggedIn()->member_id ) );
+ break;
+
+ case 'convertWarnActions':
+ $return['convertWarnActions'] = array( 'core_members_warn_actions' => NULL );
+ break;
+
+ case 'convertWarnReasons':
+ $return['convertWarnReasons'] = array( 'core_members_warn_reasons' => NULL );
+ break;
+
+ case 'convertPrivateMessages':
+ $return['convertPrivateMessages'] = array( 'core_message_topics' => NULL, 'core_message_topic_user_map' => NULL );
+ break;
+
+ case 'convertPrivateMessageReplies':
+ $return['convertPrivateMessageReplies'] = array( 'core_message_posts' => NULL );
+ break;
+
+ case 'convertModerators':
+ $return['convertModerators'] = array( 'core_moderators' => NULL );
+ break;
+
+ case 'convertProfileFieldGroups':
+ $return['convertProfileFieldGroups'] = array( 'core_pfields_groups' => NULL );
+ break;
+
+ case 'convertProfileFields':
+ if ( $method == $k )
+ {
+ foreach( \IPS\Db::i()->select( 'pf_id', 'core_pfields_data' ) AS $field )
+ {
+ \IPS\Db::i()->dropIndex( 'core_pfields_content', 'field_' . $field );
+ \IPS\Db::i()->dropColumn( 'core_pfields_content', 'field_' . $field );
+ }
+ }
+
+ $return['convertProfileFields'] = array( 'core_pfields_data' => NULL );
+ break;
+
+ case 'convertProfanityFilters':
+ $return['convertProfanityFilters'] = array( 'core_profanity_filters' => NULL );
+ break;
+
+ case 'convertQuestionAndAnswers':
+ $return['convertQuestionAndAnswers'] = array( 'core_question_and_answer' => NULL );
+ break;
+
+ case 'convertReportComments':
+ $return['convertReportComments'] = array( 'core_rc_comments' => NULL );
+ break;
+
+ case 'convertReputationLevels':
+ $return['convertReputationLevels'] = array( 'core_reputation_levels' => NULL );
+ break;
+ }
+ }
+
+ return isset( $return[ $method ] ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert an Acronym
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted acronym, or FALSE on failure
+ */
+ public function convertAcronym( $info=array() )
+ {
+ /**
+ * The below are examples of when we should use an error or a notice, depending on the situation.
+ *
+ * For example, it matters that a_id, a_short, or a_long is missing because this is required information, but we do not need to necessarily halt everything, thus a warning.
+ * a_casesensitive, however, is not and can be assumed 0 and a simple notice issued explaining.
+ */
+ if ( !isset( $info['a_id'] ) )
+ {
+ $this->software->app->log( 'acronym_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['a_short'] ) )
+ {
+ $this->software->app->log( 'acronym_missing_short', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['a_long'] ) )
+ {
+ $this->software->app->log( 'acronym_missing_long', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['a_casesensitive'] ) )
+ {
+ $this->software->app->log( 'acronym_missing_casesensitive', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $info['a_casesensitive'] = 0;
+ }
+
+ $obj = new \IPS\core\Acronym;
+ $obj->a_short = $info['a_short'];
+ $obj->a_long = $info['a_long'];
+ $obj->a_casesensitive = $info['a_casesensitive'];
+ $obj->save();
+
+ $this->software->app->addLink( $obj->a_id, $info['a_id'], 'core_acronyms' );
+ return $obj->a_id;
+ }
+
+ /**
+ * Convert an Administrator
+ *
+ * @param array $info Data to insert
+ * @return boolean TRUE on success, FALSE on failure
+ */
+ public function convertAdministrator( $info=array() )
+ {
+ if ( !isset( $info['row_id'] ) )
+ {
+ $this->software->app->log( 'administrator_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['row_id_type'] ) OR !in_array( $info['row_id_type'], array( 'member', 'group' ) ) )
+ {
+ $this->software->app->log( 'administrator_invalid_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['row_perm_cache'] ) )
+ {
+ if ( is_array( $info['row_perm_cache'] ) )
+ {
+ $info['row_perm_cache'] = json_encode( $info['row_perm_cache'] );
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'administrator_perms_missing', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['row_updated'] ) )
+ {
+ if ( $info['row_update'] instanceof \IPS\DateTIme )
+ {
+ $info['row_updated'] = $info['row_updated']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['row_updated'] = time();
+ }
+
+ switch( $info['row_id_type'] )
+ {
+ case 'member':
+ try
+ {
+ $info['row_id'] = $this->software->app->getLink( $info['row_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'administrator_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ break;
+
+ case 'group':
+ try
+ {
+ $info['row_id'] = $this->software->app->getLink( $info['row_id'], 'core_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'administrator_missing_group', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ break;
+ }
+
+ \IPS\Db::i()->insert( 'core_admin_permission_rows', $info );
+ }
+
+ /**
+ * Convert an Announcement
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted announcement, or FALSE on failure
+ */
+ public function convertAnnouncement( $info=array() )
+ {
+ if ( !isset( $info['announce_id'] ) )
+ {
+ $this->software->app->log( 'announcement_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['announce_title'] ) )
+ {
+ $this->software->app->log( 'announcement_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['announce_content'] ) )
+ {
+ $this->software->app->log( 'announcement_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['announce_member_id'] ) )
+ {
+ $this->software->app->log( 'announcement_missing_member_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['announce_views'] ) )
+ {
+ $info['announce_views'] = 0;
+ }
+
+ if ( !isset( $info['announce_start'] ) )
+ {
+ $info['announce_start'] = time();
+ }
+
+ if ( !isset( $info['announce_end'] ) )
+ {
+ $info['announce_end'] = 0;
+ }
+
+ if ( !isset( $info['announce_active'] ) )
+ {
+ $info['announce_active'] = 0;
+ }
+
+ if ( !isset( $info['announce_seo_title'] ) )
+ {
+ $info['announce_seo_title'] = \IPS\Http\Url::seoTitle( $info['announce_title'] );
+ }
+
+ if ( !isset( $info['announce_ids'] ) )
+ {
+ $info['announce_ids'] = NULL;
+ }
+
+ if ( !isset( $info['announce_app'] ) )
+ {
+ $info['announce_app'] = '*';
+ }
+
+ if ( !isset( $info['announce_location'] ) )
+ {
+ $info['announce_location'] = '*';
+ }
+
+ $obj = new \IPS\core\Announcements\Announcement;
+ foreach ( $info AS $field => $value)
+ {
+ if ( $field !== 'announce_id' )
+ {
+ $field = str_replace( 'announce_', '', $field );
+ $obj->$field = $value;
+ }
+
+ }
+ $obj->save();
+
+ $this->software->app->addLink( $obj->id, $info['announce_id'], 'core_announcement' );
+ return $obj->a_id;
+
+ }
+
+ /**
+ * Convert an Attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Array of Map data
+ * @param NULL|string $filepath The path to the attachment files or NULL if loading from the database.
+ * @param NULL|string $filedata If loading from the database, the content of the Binary column.
+ * @return boolean|integer The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['attach_id'] ) )
+ {
+ $this->software->app->log( 'attachment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( is_null( $filepath ) AND is_null( $filedata ) )
+ {
+ $this->software->app->log( 'attachment_missing_data', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ }
+
+ if ( is_null( $filedata ) AND !file_exists( $filepath ) )
+ {
+ $this->software->app->log( 'attachment_file_missing', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+
+ /* All attachments must have at least a location key and id1 OR id3 */
+ if ( !isset( $map['location_key'] ) AND ( !isset( $map['id1'] ) OR !isset( $map['id3'] ) ) )
+ {
+ $this->software->app->log( 'attachment_missing_map_data', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+
+ /* Make sure our location key is valid */
+ $haveExtension = FALSE;
+ foreach( array_keys( \IPS\Application::allExtensions( 'core', 'EditorLocations', FALSE, NULL, NULL, FALSE ) ) AS $extension )
+ {
+ if ( $map['location_key'] == $extension )
+ {
+ $haveExtension = TRUE;
+ break;
+ }
+ }
+
+ if ( $haveExtension === FALSE )
+ {
+ $this->software->app->log( 'attachment_invalid_location', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+
+ /* Most of the rest of the information we need can be automatically determined if it's not present */
+ if ( !isset( $info['attach_file'] ) )
+ {
+ $fileName = explode( '/', $info['attach_location'] );
+ $fileName = array_pop( $fileName );
+ $info['attach_file'] = $fileName;
+ }
+
+ if ( !isset( $info['attach_ext'] ) )
+ {
+ $extension = explode( ',', $info['attach_file'] );
+ $extension = array_pop( $extension );
+ $info['attach_ext'] = $extension;
+ }
+
+ if ( isset( $info['attach_date'] ) )
+ {
+ if ( $info['attach_date'] instanceof \IPS\DateTime )
+ {
+ $info['attach_date'] = $info['attach_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['attach_date'] = time();
+ }
+
+ /* These don't matter */
+ $info['attach_post_key'] = '';
+ $info['attach_is_archived'] = 0;
+
+ if ( isset( $info['attach_member_id'] ) )
+ {
+ try
+ {
+ $info['attach_member_id'] = $this->software->app->getLink( $info['attach_member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['attach_member_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['attach_member_id'] = 0;
+ }
+
+ if ( !isset( $info['attach_hits'] ) )
+ {
+ $info['attach_hits'] = 0;
+ }
+
+ /* Create the file */
+ try
+ {
+ $done = FALSE;
+ /* We need to do some re-jiggery here - loading the entire file into memory can cause exhaustion and other issue... so, if the core_Attachments extension is using the local file system, lets copy it temporarily and move it that way */
+ $upload_settings = json_decode( \IPS\Settings::i()->upload_settings, TRUE );
+ if ( isset( $upload_settings['filestorage__core_Attachment'] ) AND $filepath )
+ {
+ try
+ {
+ $storage = \IPS\Db::i()->select( '*', 'core_file_storage', array( "id=?", $upload_settings['filestorage__core_Attachment'] ) )->first();
+
+ if ( $storage['method'] == 'FileSystem' )
+ {
+ $configuration = json_decode( $storage['configuration'], TRUE );
+
+ if ( isset( $configuration['dir'] ) )
+ {
+ $storage_dir = str_replace( '{root}', \IPS\ROOT_PATH, $configuration['dir'] );
+
+ if ( !@is_dir( $storage_dir . '/convert_temp' ) )
+ {
+ if ( @mkdir( $storage_dir . '/convert_temp' ) )
+ {
+ @chmod( $storage_dir . '/convert_temp', \IPS\IPS_FOLDER_PERMISSION );
+ }
+ else
+ {
+ $last_error = @error_get_last();
+ throw new \RuntimeException( $last_error['message'] );
+ }
+ }
+
+ if ( !@is_dir( $storage_dir . '/convert_temp' ) )
+ {
+ $last_error = @error_get_last();
+ throw new \RuntimeException( $last_error['message'] );
+ }
+ /* Now copy the file to our temp directory */
+ if ( @copy( $filepath, $storage_dir . '/convert_temp/' . $info['attach_file'] ) )
+ {
+ /* Good, now let's move it properly. Exceptions will bubble up. */
+ $file = \IPS\File::create( 'core_Attachments', $info['attach_file'], NULL, 'monthly_' . date( 'Y', $info['attach_date'] ) . '_' . date( 'm', $info['attach_date'] ), TRUE, $storage_dir . '/convert_temp/' . $info['attach_file'] );
+ @unlink( $storage_dir . '/convert_temp/' . $info['attach_file'] );
+ $done = TRUE;
+
+ /* Unset filedata to conserve memory */
+ unset( $filedata );
+ }
+ else
+ {
+ $last_error = @error_get_last();
+ throw new \RuntimeException( $last_error['message'] );
+ }
+ }
+ }
+ }
+ /* We specifically look for exceptions here, but do not halt the process - we can still try the old method with the file data in memory. */
+ catch( \UnderflowException $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+ }
+ catch( \RunimeException $e )
+ {
+ \IPS\Log::log( $e, 'converters' );
+ }
+ }
+
+ if ( $done === FALSE )
+ {
+ /* Last ditch effort to use what is in memory */
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = file_get_contents( $filepath );
+ }
+ }
+ catch( \ErrorException $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+ $file = \IPS\File::create( 'core_Attachments', $info['attach_file'], $filedata, 'monthly_' . date( 'Y', $info['attach_date'] ) . '_' . date( 'm', $info['attach_date'] ), TRUE );
+ }
+
+ $info['attach_location'] = (string) $file;
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( 'attachment_creation_fail', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+ catch( \ErrorException $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['attach_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['attach_filesize'] ) )
+ {
+ $info['attach_filesize'] = $file->filesize();
+ }
+
+ /* If this is an image, we need to do stuff */
+ if ( $file->isImage() )
+ {
+ /* Make sure the image flag is set */
+ $info['attach_is_image'] = 1;
+
+ /* Height and Width of the main image */
+ if ( !isset( $info['attach_img_width'] ) OR !isset( $info['attach_img_height'] ) )
+ {
+ try
+ {
+ $dimensions = $file->getImageDimensions();
+
+ if ( !isset( $info['attach_img_width'] ) )
+ {
+ $info['attach_img_width'] = $dimensions[0];
+ }
+
+ if ( !isset( $info['attach_img_height'] ) )
+ {
+ $info['attach_img_height'] = $dimensions[1];
+ }
+ }
+ catch( \InvalidArgumentException $e )
+ {
+ /* File isn't actually an image. */
+ $info['attach_is_image'] = 0;
+ $info['attach_img_width'] = 0;
+ $info['attach_img_height'] = 0;
+ }
+ }
+ }
+ else
+ {
+ $info['attach_thumb_location'] = '';
+ $info['attach_thumb_width'] = 0;
+ $info['attach_thumb_height'] = 0;
+ $info['attach_is_image'] = 0;
+ $info['attach_img_width'] = 0;
+ $info['attach_img_height'] = 0;
+ }
+
+ $id = $info['attach_id'];
+ unset( $info['attach_id'] );
+
+ try
+ {
+ $inserted_id = \IPS\Db::i()->insert( 'core_attachments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_attachments' );
+ }
+ catch( \IPS\Db\Exception $e )
+ {
+ $this->software->app->log( 'attachment_invalid_data', __METHOD__, \IPS\convert\App::LOG_WARNING, $id );
+ return FALSE;
+ }
+
+ /* Now we need to figure out map information */
+ if ( !isset( $map['id1'] ) AND isset( $map['id3'] ) )
+ {
+ /* This is just a key - we can directly insert it. The converter will need to determine how to translate */
+ \IPS\Db::i()->insert( 'core_attachments_map', array(
+ 'attachment_id' => $inserted_id,
+ 'location_key' => $map['location_key'],
+ 'id1' => NULL,
+ 'id2' => NULL,
+ 'temp' => NULL,
+ 'id3' => $map['id3']
+ ) );
+
+ return $inserted_id;
+ }
+
+ /* This gets a bit tricky... we cannot automatically determine where our ID is from, so we need extra information to be passed by the converter */
+ try
+ {
+ $id1 = $this->software->app->getLink( $map['id1'], $map['id1_type'], $map['id1_from_parent'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ \IPS\Db::i()->delete( 'core_attachments', array( "attach_id=?", $id ) );
+ $this->software->app->log( 'attachment_missing_parent', __METHOD__, \IPS\convert\App::LOG_WARNING, $id );
+ return;
+ }
+
+ if ( isset( $map['id2'] ) )
+ {
+ try
+ {
+ $id2 = $this->software->app->getLink( $map['id2'], $map['id2_type'], $map['id2_from_parent'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $id2 = NULL;
+ }
+ }
+ else
+ {
+ $id2 = NULL;
+ }
+
+ if ( isset( $map['id3'] ) AND ( !isset( $map['id3_skip_link'] ) OR $map['id3_skip_link'] === FALSE ) )
+ {
+ try
+ {
+ $id3 = $this->software->app->getLink( $map['id3'], $map['id3_type'], $map['id3_from_parent'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $id3 = NULL;
+ }
+ }
+ else
+ {
+ $id3 = NULL;
+ }
+
+ \IPS\Db::i()->insert( 'core_attachments_map', array(
+ 'attachment_id' => $inserted_id,
+ 'location_key' => $map['location_key'],
+ 'id1' => $id1,
+ 'id2' => $id2,
+ 'temp' => NULL,
+ 'id3' => $id3,
+ ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Ban Filter
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted ban filter, or FALSE on failure
+ */
+ public function convertBanfilter( $info=array() )
+ {
+ if ( !isset( $info['ban_id'] ) )
+ {
+ $this->software->app->log( 'banfilter_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['ban_type'] ) OR !in_array( $info['ban_type'], array( 'ip', 'email', 'name' ) ) )
+ {
+ $this->software->app->log( 'banfilter_missing_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['ban_content'] ) )
+ {
+ $this->software->app->log( 'banfilter_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['ban_date'] ) )
+ {
+ $this->software->app->log( 'banfilter_missing_date', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $info['ban_date'] = time();
+ }
+
+ if ( !isset( $info['ban_reason'] ) )
+ {
+ $info['ban_reason'] = '';
+ }
+
+ $old_id = $info['ban_id'];
+ unset( $info['ban_id'] );
+ $inserted_id = \IPS\Db::i()->insert( 'core_banfilters', $info );
+ $this->software->app->addLink( $inserted_id, $old_id, 'core_banfilters' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert Custom BBCode for the LegacyParser to use
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted BBCode, or FALSE on failure.
+ */
+ public function convertCustomBbcode( $info=array() )
+ {
+ if ( !isset( $info['bbcode_id'] ) )
+ {
+ $this->software->app->log( 'bbcode_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['bbcode_tag'] ) )
+ {
+ $this->software->app->log( 'bbcode_missing_tag', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['bbcode_replacement'] ) )
+ {
+ $this->software->app->log( 'bbcode_missing_replacement', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['bbcode_title'] ) )
+ {
+ $this->software->app->log( 'bbcode_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Optional data */
+ if( !isset( $info['bbcode_description'] ) )
+ {
+ $info['bbcode_description'] = '';
+ }
+ if( !isset( $info['bbcode_example'] ) )
+ {
+ $info['bbcode_example'] = '';
+ }
+
+ if( !isset( $info['bbcode_useoption'] ) )
+ {
+ $info['bbcode_useoption'] = 0;
+ }
+
+ $insertedId = \IPS\Db::i()->insert( 'convert_custom_bbcode',
+ array(
+ 'bbcode_title' => $info['bbcode_title'],
+ 'bbcode_desc' => $info['bbcode_description'],
+ 'bbcode_tag' => $info['bbcode_tag'],
+ 'bbcode_replace' => $info['bbcode_replacement'],
+ 'bbcode_example' => $info['bbcode_example'],
+
+ /* For the purpose of using this for the parse only, these items will be set globally */
+ 'bbcode_groups' => 'all',
+ 'bbcode_sections' => 'all',
+ 'bbcode_app' => 'core'
+ )
+ );
+
+ $this->software->app->addLink( $insertedId, $info['bbcode_id'], 'core_bbcode' );
+ return $insertedId;
+ }
+
+ /**
+ * Convert Display Name History
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Display Name History, or FALSE on failure.
+ */
+ public function convertDnameChange( $info=array() )
+ {
+ // not sure if we really need this?
+ if ( !isset( $info['old_id'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_old_id', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ }
+
+ if ( !isset( $info['member_id'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_member_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ try
+ {
+ $newMemberId = $this->software->app->getLink( $info['member_id'], 'core_member' );
+ }
+ catch ( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'dname_change_not_existing_member', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+
+ if ( !isset( $info['dname_previous'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_dname_previous', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['dname_current'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_dname_current', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['dname_date'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_dname_current', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $info['dname_date'] = time();
+ }
+
+ if ( !isset( $info['dname_ip_address'] ) )
+ {
+ $this->software->app->log( 'dname_change_missing_dname_ip_address', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $info['dname_ip_address'] =\IPS\Request::i()->ipAddress();
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_dnames_change', array(
+ 'dname_member_id' => $newMemberId,
+ 'dname_date' => $info['dname_date'],
+ 'dname_ip_address' => $info['dname_ip_address'],
+ 'dname_previous' => $info['dname_previous'],
+ 'dname_current' => $info['dname_current'],
+ 'dname_discount' => FALSE,
+ )
+ );
+
+
+ $this->software->app->addLink( $inserted_id, $info['old_id'], 'core_dname_change' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Edit History Log
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Edit History, or FALSE on failure.
+ */
+ public function convertEditHistory( $info=array() )
+ {
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'edit_history_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['class'] ) OR !in_array( $info['class'], \IPS\Content::routedClasses() ) )
+ {
+ $this->software->app->log( 'edit_history_missing_class', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( !in_array( 'IPS\Content\EditHistory', class_implements( $info['class'] ) ) )
+ {
+ $this->software->app->log( 'edit_history_not_supported', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ $classname = $info['class'];
+
+ if ( isset( $info['comment_id'] ) )
+ {
+ try
+ {
+ $info['comment_id'] = $this->software->app->getLink( $info['comment_id'], $classname::$databaseTable );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'edit_history_missing_comment', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'edit_history_missing_comment', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['member'] ) )
+ {
+ try
+ {
+ $info['member'] = $this->software->app->getLink( $info['member'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['member'] = 0;
+ }
+ }
+ else
+ {
+ $info['member'] = 0;
+ }
+
+ if ( isset( $info['time'] ) )
+ {
+ if ( $info['time'] instanceof \IPS\DateTime )
+ {
+ $info['time'] = $info['time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['time'] = time();
+ }
+
+ if ( !isset( $info['old'] ) )
+ {
+ $this->software->app->log( 'edit_history_missing_old', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['id'] );
+ $info['old'] = '';
+ }
+
+ if ( !isset( $info['new'] ) )
+ {
+ $this->software->app->log( 'edit_history_missing_new', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['id'] );
+ $info['new'] = '';
+ }
+
+ if ( isset( $info['public'] ) )
+ {
+ $info['public'] = (boolean) $info['public'];
+ }
+ else
+ {
+ $info['public'] = 0;
+ }
+
+ if ( !isset( $info['reason'] ) )
+ {
+ $info['reason'] = NULL;
+ }
+
+ $id = $info['id'];
+ unset( $info['id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_edit_history', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_edit_history' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Emoticon
+ *
+ * @param array $info Data to insert
+ * @param NULL|array $set Set to store this emoticon in.
+ * @param boolean $merge If TRUE, then if an emoticon code already exists in our database, we'll keep that one. FALSE means overwrite it.
+ * @param NULL|string $filepath Path to files, or NULL if loading from the database.
+ * @param NULL|string $filedata If loading from the database, the content of the Binary column.
+ * @param NULL|string $filepathx2 Path to the x2 size emoticon, or NULL if it doesn't exist.
+ * @param NULL|string $filedatax2 File Data for the x2 size emoticon, or NULL if it doesn't exist.
+ * @return boolean|integer The ID of the newly inserted emoticon, or FALSE on failure.
+ * @todo Handle emoticons without a defined set.
+ */
+ public function convertEmoticon( $info=array(), $set=NULL, $keepExisting=TRUE, $filepath=NULL, $filedata=NULL, $filepathx2=NULL, $filedatax2=NULL )
+ {
+ /* We don't really need an ID for these */
+ $haveID = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'emoticon_missing_ids', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $haveID = FALSE;
+ }
+
+ /* We do need these, though */
+ if ( !isset( $info['typed'] ) )
+ {
+ $this->software->app->log( 'emoticon_missing_code', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $haveID ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( $keepExisting === TRUE )
+ {
+ /* Do we already have an emoticon for this code? */
+ try
+ {
+ $existing = \IPS\Db::i()->select( '*', 'core_emoticons', array( "typed=?", $info['typed'] ) )->first();
+
+ if ( $haveID )
+ {
+ $this->software->app->addLink( $existing['id'], $info['id'], 'core_emoticons', TRUE );
+ }
+
+ return $existing['id'];
+ }
+ catch( \UnderflowException $e ) {} # lookup failed, don't do anything as it means we need to insert normally
+ }
+ else
+ {
+ /* We are using the source - we need to remove any for this typed code */
+ \IPS\Db::i()->delete( 'core_emoticons', array( "typed=?", $info['typed'] ) );
+ }
+
+ if ( is_null( $filepath ) AND is_null( $filedata ) )
+ {
+ $this->software->app->log( 'emoticon_no_file', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $haveID ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $info['filename'] ) )
+ {
+ $filedata = @file_get_contents( rtrim( $filepath, '/' ) . '/' . $info['filename'] );
+ $filepath = NULL;
+ }
+ else
+ {
+ $this->software->app->log( 'emoticon_no_file', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $haveID ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+
+ if ( !isset( $info['filename'] ) )
+ {
+ $this->software->app->log( 'emoticon_no_filename', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $haveID ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['clickable'] ) )
+ {
+ $info['clickable'] = 1;
+ }
+
+ $info['emo_set'] = $set['set'];
+ $info['emo_set_position'] = $set['position'];
+
+ if ( \IPS\Member::loggedIn()->language()->checkKeyExists( "core_emoticon_group_{$info['emo_set']}" ) == FALSE )
+ {
+ \IPS\Lang::saveCustom( 'core', "core_emoticon_group_{$info['emo_set']}", $set['title'] );
+ }
+
+ if ( !isset( $info['emo_position'] ) )
+ {
+ $newPosition = (int) \IPS\Db::i()->select( 'MAX(emo_position) + 1', 'core_emoticons', array( 'emo_set=?', $set['set'] ) )->first();
+ $info['emo_position'] = $newPosition;
+ }
+
+ try
+ {
+ $file = \IPS\File::create( 'core_Emoticons', $info['filename'], $filedata, 'emoticons' );
+ unset( $info['filename'] );
+ $info['image'] = (string) $file;
+ $dims = $file->getImageDimensions();
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( 'emoticon_file_corrupt', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $haveID ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['width'] ) )
+ {
+ $info['width'] = $dims[0];
+ }
+
+ if ( !isset( $info['height'] ) )
+ {
+ $info['height'] = $dims[1];
+ }
+
+ if ( isset( $info['filenamex2'] ) AND ( !is_null( $filedatax2 ) OR !is_null( $filepathx2 ) ) )
+ {
+ try
+ {
+ if ( is_null( $filedatax2 ) AND !is_null( $filepathx2 ) )
+ {
+ $filedatax2 = file_get_contents( rtrim( $filepathx2, '/' ) . '/' . $info['filenamex2'] );
+ $filepathx2 = NULL;
+ }
+ $filex2 = \IPS\File::create( 'core_Emoticons', $info['filenamex2'], $filedatax2, 'emoticons' );
+ unset( $info['filenamex2'] );
+ $info['image_2x'] = (string) $filex2;
+ }
+ catch( \Exception $e )
+ {
+ $info['image_2x'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['image_2x'] = NULL;
+ }
+ }
+
+ if ( $haveID )
+ {
+ $id = $info['id'];
+ unset( $info['id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_emoticons', $info );
+
+ if ( $haveID )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_emoticons' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert Follow Data
+ *
+ * @param array $info Data to insert
+ * @return boolean Unlike other methods, we do not need to return an ID for the converted follow - so simply return TRUE on success, or FALSE on failure.
+ * @note This method should not have an individual step, but rather be called during others (ex. when converting topics, insert any follows at that point)
+ */
+ public function convertFollow( $info=array() )
+ {
+ if ( !isset( $info['follow_app'] ) )
+ {
+ $this->software->app->log( 'follow_missing_app', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !\IPS\Application::appIsEnabled( $info['follow_app'] ) )
+ {
+ $this->software->app->log( 'follow_app_disabed', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['follow_area'] ) )
+ {
+ $this->software->app->log( 'follow_missing_area', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Like attachments, we need a bit more information at runtime about where we need to lookup our link reference */
+ if ( !isset( $info['follow_rel_id'] ) OR !isset( $info['follow_rel_id_type'] ) )
+ {
+ $this->software->app->log( 'follow_missing_rel_info', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['follow_rel_id'] = $this->software->app->getLink( $info['follow_rel_id'], $info['follow_rel_id_type'] );
+ unset( $info['follow_rel_id_type'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'follow_missing_rel_info_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['follow_member_id'] ) )
+ {
+ try
+ {
+ $info['follow_member_id'] = $this->software->app->getLink( $info['follow_member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'follow_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'follow_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Generic Stuff */
+ $info['follow_is_anon'] = ( isset( $info['follow_is_anon'] ) ) ? $info['follow_is_anon'] : 0;
+ $info['follow_notify_do'] = ( isset( $info['follow_notify_do'] ) ) ? $info['follow_notify_do'] : 0;
+ $info['follow_notify_meta'] = ( isset( $info['follow_notify_meta'] ) ) ? ( is_array( $info['follow_notify_meta'] ) ? json_encode( $info['follow_notify_meta'] ) : $info['follow_notify_meta'] ) : '';
+ $info['follow_notify_freq'] = ( isset( $info['follow_notify_freq'] ) AND in_array( $info['follow_notify_freq'], array( 'none', 'immediate', 'daily', 'weekly' ) ) ) ? $info['follow_notify_freq'] : 'none';
+ $info['follow_visible'] = ( isset( $info['follow_visible'] ) ) ? $info['follow_visible'] : 1;
+ $info['follow_index_id'] = ( isset( $info['follow_index_id'] ) ) ? $info['follow_index_id'] : NULL; // @todo Figure out what this does
+
+ /* DateTime Stuff */
+ if ( isset( $info['follow_added'] ) )
+ {
+ if ( $info['follow_added'] instanceof \IPS\DateTime )
+ {
+ $info['follow_added'] = $info['follow_added']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['follow_added'] = time();
+ }
+
+ if ( isset( $info['follow_notify_sent'] ) )
+ {
+ if ( $info['follow_notify_sent'] instanceof \IPS\DateTime )
+ {
+ $info['follow_notify_sent'] = $info['follow_notify_sent']->getTimestamp();
+ }
+ }
+ else
+ {
+ /* We set time() here as we may not know when the last notification was sent - we don't want to send another */
+ $info['follow_notify_sent'] = time();
+ }
+
+ /* Generate follow ID */
+ $info['follow_id'] = md5( $info['follow_app'] . ';' . $info['follow_area'] . ';' . $info['follow_rel_id'] . ';' . $info['follow_member_id'] );
+
+ /* Duplicate? */
+ try
+ {
+ $dupe = \IPS\Db::i()->select( '*', 'core_follow', array( "follow_id=?", $info['follow_id'] ) )->first();
+
+ if ( $dupe['follow_id'] )
+ {
+ return TRUE;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ try
+ {
+ \IPS\Db::i()->insert( 'core_follow', $info );
+ return TRUE;
+ }
+ catch( \IPS\Db\Exception $e )
+ {
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'follow_failed_insert' ), $e->getMessage() ), __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+
+ /**
+ * @brief Bitwise are protected in \IPS\Member\Group, so copying here.
+ */
+ protected static $groupBitOptions = array(
+ 'gbw_mod_post_unit_type' => 1, // Lift moderation after x. 1 is days, 0 is posts. Corresponds to g_mod_post_unit
+ 'gbw_ppd_unit_type' => 2, // Lift post-per-day limit after x. 1 is days, 0 is posts. Corresponds to g_ppd_unit
+ 'gbw_displayname_unit_type' => 4, // Username change restrictions. 1 is days, 0 is posts. Corresponds to g_displayname_unit
+ 'gbw_sig_unit_type' => 8, // Signature edit restrictions. 1 is days, 0 is posts. Corresponds to g_sig_unit
+ 'gbw_promote_unit_type' => 16, // Type of group promotion to use. 1 is days since joining, 0 is content count. Corresponds to g_promotion
+ 'gbw_no_status_update' => 32, // Can NOT post status updates
+ // 64 is deprecated (previously gbw_soft_delete)
+ 'gbw_soft_delete_own' => 128, // Allow users of this group to hide their own submitted content
+ // 256 is deprecated (previously gbw_soft_delete_own_topic)
+ // 512 is deprecated (previously gbw_un_soft_delete)
+ // 1024 is deprecated (previously gbw_soft_delete_see)
+ // 2048 is deprecated (previously gbw_soft_delete_topic)
+ // 4096 is deprecated (previously gbw_un_soft_delete_topic)
+ // 8192 is deprecated (previously gbw_soft_delete_topic_see)
+ // 16384 is deprecated (previously gbw_soft_delete_reason)
+ // 32768 is deprecated (previously gbw_soft_delete_see_post)
+ // 65536 is deprecated (previously gbw_allow_customization)
+ // 131072 is deprecated (previously gbw_allow_url_bgimage)
+ 'gbw_allow_upload_bgimage' => 262144, // Can upload a cover photo?
+ 'gbw_view_reps' => 524288, // Can view who gave reputation?
+ 'gbw_no_status_import' => 1048576, // Can NOT import status updates from Facebook/Twitter
+ 'gbw_disable_tagging' => 2097152, // Can NOT use tags
+ 'gbw_disable_prefixes' => 4194304, // Can NOT use prefixes
+ // 8388608 is deprecated (previously gbw_view_last_info)
+ // 16777216 is deprecated (previously gbw_view_online_lists)
+ // 33554432 is deprecated (previously gbw_hide_leaders_page)
+ 'gbw_pm_unblockable' => 67108864, // Deprecated in favour of global unblockable setting
+ 'gbw_pm_override_inbox_full'=> 134217728, // 1 means this group can send other members PMs even when that member's inbox is full
+ 'gbw_no_report' => 268435456, // 1 means they CAN'T report content. 0 means they can.
+ 'gbw_cannot_be_ignored' => 536870912, // 1 means they cannot be ignored. 0 means they can
+ 'gbw_delete_attachments' => 1073741824, // 1 means they can delete attachments from the "My Attachments" screen
+ );
+
+ /**
+ * Convert a Member Group
+ *
+ * @param array $info Data to insert
+ * @param integer|NULL $mergeWith THe ID of the group to merge this one into, or NULL to create new.
+ * @param string|NULL $iconpath Path to Icon file for the group, or NULL.
+ * @param string|NULL $icondata Binary Icon Data, or NULL.
+ * @return boolean|integer The ID of the newly inserted group, or FALSE on failure.
+ * @note Other libraries will handle their own settings
+ */
+ public function convertGroup( $info=array(), $mergeWith=NULL, $iconpath=NULL, $icondata=NULL )
+ {
+ if ( !isset( $info['g_id'] ) )
+ {
+ $this->software->app->log( 'group_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Are we merging this group with an existing one? Saves a lot of headache later */
+ if ( !is_null( $mergeWith ) )
+ {
+ $this->software->app->addLink( $mergeWith, $info['g_id'], 'core_groups', TRUE );
+ return $mergeWith;
+ }
+
+ /* Do specialty stuff first, then we can just go over generic stuff later */
+ if ( !isset( $info['g_name'] ) )
+ {
+ $name = "Untitled Group {$info['g_id']}";
+ $this->software->app->log( 'group_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ }
+ else
+ {
+ $name = $info['g_name'];
+ unset( $info['g_name'] );
+ }
+
+ if ( isset( $info['g_icon'] ) AND ( !is_null( $iconpath ) OR !is_null( $icondata ) ) )
+ {
+ try
+ {
+ if ( is_null( $icondata ) AND !is_null( $iconpath ) )
+ {
+ $icondata = file_get_contents( $iconpath );
+ $iconpath = NULL;
+ }
+ $file = \IPS\File::create( 'core_Theme', $info['g_icon'], $icondata );
+ $info['g_icon'] = (string) $file;
+ }
+ catch( \Exception $e )
+ {
+ $info['g_icon'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['g_icon'] = NULL;
+ }
+ }
+ else
+ {
+ $info['g_icon'] = NULL;
+ }
+
+ if ( isset( $info['g_promotion'] ) )
+ {
+ if ( is_array( $info['g_promotion'] ) )
+ {
+ try
+ {
+ $group = $this->software->app->getLink( $info['g_promotion'][0], 'core_groups' );
+ $promotion = $group . '&' . $info['g_promotion'][1];
+ $info['g_promotion'] = $promotion;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Not much we can do here... */
+ $info['g_promotion'] = '-1&-1';
+ }
+ }
+ else
+ {
+ try
+ {
+ $promotion = explode( '&', $info['g_promotion'] );
+ $group = $this->software->app->getLink( $promotion[0], 'core_groups' );
+ $info['g_promotion'] = $group . '&' . $promotion[1];
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['g_promotion'] = '-1&-1';
+ }
+ }
+ }
+ else
+ {
+ $info['g_promotion'] = '-1&-1';
+ }
+
+ if ( isset( $info['g_photo_max_vars'] ) )
+ {
+ if ( is_array( $info['g_photo_max_vars'] ) )
+ {
+ $info['g_photo_max_vars'] = implode( ':', $info['g_photo_max_vars'] );
+ }
+ }
+ else
+ {
+ $info['g_photo_max_vars'] = '500:170:170';
+ }
+
+ if ( isset( $info['g_signature_limits'] ) )
+ {
+ if ( is_array( $info['g_signature_limits'] ) )
+ {
+ $info['g_signature_limits'] = implode( ':', $info['g_signature_limits'] );
+ }
+ }
+ else
+ {
+ $info['g_signature_limits'] = '0:::::';
+ }
+
+ $bitoptions = 0;
+ if ( isset( $info['g_bitoptions'] ) AND is_array( $info['g_bitoptions'] ) )
+ {
+ foreach( static::$groupBitOptions AS $key => $value )
+ {
+ if ( isset( $info['g_bitoptions'][$key] ) AND $info['g_bitoptions'][$key] == TRUE )
+ {
+ $bitoptions += $value;
+ }
+ }
+ }
+ $info['g_bitoptions'] = $bitoptions;
+
+ /* Now let's do generic stuff - start with things that should be 1 as default for our default members group */
+ foreach( array( 'g_view_board', 'g_mem_info', 'g_use_search', 'g_edit_profile', 'g_edit_posts', 'g_use_pm', 'g_can_msg_attach', 'g_dname_date', 'g_pm_flood_mins', 'g_post_polls', 'g_vote_polls' ) AS $oneIsDefault )
+ {
+ if ( !isset( $info[$oneIsDefault] ) )
+ {
+ $info[$oneIsDefault] = 1;
+ }
+ }
+
+ /* Now let's do 0 defaults */
+ foreach( array( 'g_delete_own_posts', 'g_is_supmod', 'g_access_cp', 'g_append_edit', 'g_access_offline', 'g_avoid_q', 'g_avoid_flood', 'g_max_messages', 'g_dohtml', 'g_bypass_badwords', 'g_attach_per_post', 'g_dname_changes', 'g_mod_preview', 'g_hide_online_list', 'g_mod_post_unit', 'g_ppd_limit', 'g_ppd_unit', 'g_displayname_unit', 'g_sig_unit', 'g_topic_rate_setting' ) AS $zeroIsDefault )
+ {
+ if ( !isset( $info[$zeroIsDefault] ) )
+ {
+ $info[$zeroIsDefault] = 0;
+ }
+ }
+
+ /* -1 Defaults */
+ foreach( array( 'g_attach_max', 'g_max_bgimg_upload' ) AS $negOneDefault )
+ {
+ if ( !isset( $info[$negOneDefault] ) )
+ {
+ $info[$negOneDefault] = -1;
+ }
+ }
+
+ /* Other defaults */
+ foreach( array( 'prefix', 'suffix', 'g_max_mass_pm', 'g_search_flood', 'g_edit_cutoff', 'g_rep_max_positive', 'g_rep_max_negative', 'g_pm_perday' ) AS $otherDefault )
+ {
+ if ( !isset( $info[$otherDefault] ) )
+ {
+ switch( $otherDefault )
+ {
+ case 'prefix':
+ case 'suffix':
+ $info[$otherDefault] = '';
+ break;
+
+ case 'g_max_mass_pm':
+ $info[$otherDefault] = 10;
+ break;
+
+ case 'g_search_flood':
+ $info[$otherDefault] = 3;
+ break;
+
+ case 'g_edit_cutoff':
+ $info[$otherDefault] = 5;
+ break;
+
+ case 'g_rep_max_positive':
+ case 'g_rep_max_negative':
+ $info[$otherDefault] = 10;
+ break;
+ }
+ }
+ }
+
+ /* Unlike the other apps, we do Chat here as we can't really convert anything else from it */
+ if ( \IPS\Application::appisEnabled( 'chat' ) )
+ {
+ if ( !isset( $info['chat_access'] ) )
+ {
+ $info['chat_access'] = 1;
+ }
+
+ if ( !isset( $info['chat_moderate'] ) )
+ {
+ $info['chat_moderate'] = 0;
+ }
+
+ if ( !isset( $info['chat_private'] ) )
+ {
+ $info['chat_private'] = 0;
+ }
+ }
+ else
+ {
+ /* Unset them to avoid DB errors */
+ unset( $info['chat_access'], $info['chat_moderate'], $info['chat_private'] );
+ }
+
+ /* Save it! */
+ $id = $info['g_id'];
+ unset( $info['g_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_groups', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_groups' );
+ \IPS\Lang::saveCustom( 'core', "core_group_{$inserted_id}", $name );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Ignored User
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Ignore, or FALSE on error.
+ */
+ public function convertIgnoredUser( $info=array() )
+ {
+ if ( !isset( $info['ignore_id'] ) )
+ {
+ $this->software->app->log( 'ignored_user_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['ignore_owner_id'] ) )
+ {
+ try
+ {
+ $owner = $this->software->app->getLink( $info['ignore_owner_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'ignored_user_missing_owner', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'ignored_user_missing_owner', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['ignore_ignore_id'] ) )
+ {
+ try
+ {
+ $ignore = $this->software->app->getLink( $info['ignore_ignore_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'ignored_user_missing_ignore', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'ignored_user_missing_ignore', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ $obj = new \IPS\core\Ignore;
+ $obj->owner_id = $owner;
+ $obj->ignore_id = $ignore;
+
+ foreach( \IPS\core\Ignore::types() AS $type )
+ {
+ /* If the type is set and evaluates to true, then set to 1, otherwise 0. */
+ $obj->$type = ( isset( $info['ignore_' . $type] ) AND $info['ignore_' . $type] ) ? 1 : 0;
+ }
+
+ $obj->save();
+ $this->software->app->addLink( $obj->id, $info['ignore_id'], 'core_ignored_users' );
+ return $obj->id;
+ }
+
+ /**
+ * Convert a Staff Directory Entry
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Leader, or FALSE on failure.
+ */
+ public function convertLeader( $info=array() )
+ {
+ if ( !isset( $info['leader_id'] ) )
+ {
+ $this->software->app->log( 'leader_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['leader_type'] ) )
+ {
+ $this->software->app->log( 'leader_missing_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['leader_type_id'] ) )
+ {
+ $this->software->app->log( 'leader_missing_type_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ switch( $info['leader_type'] )
+ {
+ case 'g':
+ case 'group':
+ try
+ {
+ $group = $this->software->app->getLink( $info['leader_type_id'], 'core_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'leader_missing_group_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ $info['leader_type'] = 'g';
+ $info['leader_type_id'] = $group;
+ break;
+
+ case 'm':
+ case 'members':
+ try
+ {
+ $member = $this->software->app->getLink( $info['leader_type_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'leader_missing_member_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ $info['leader_type'] = 'm';
+ $info['leader_type_id'] = $member;
+ break;
+ }
+
+ /* This is technically missing required data, however we can create a new parent object in this instance, and avoid data loss */
+ try
+ {
+ if ( !isset( $info['leader_group_id'] ) )
+ {
+ /* If it's not set throw an exception to try and trigger orphan parent detection */
+ throw new \DomainException;
+ }
+
+ $info['leader_group_id'] = $this->software->app->getLink( $info['leader_group_id'], 'core_leader_groups' );
+ }
+ catch( \LogicException $e ) /* LogicException here so as to accommodate for catching both OutOfRangeException and DomainException - we don't care which was thrown */
+ {
+ try
+ {
+ /* Have we already created an orphaned row storage container? */
+ $info['leader_group_id'] = $this->software->app->getLink( '__orphan__', 'core_leader_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* If we are creating, we should do it in the sense that we are converting so we can use it later on */
+ $info['leader_group_id'] = $this->convertLeaderGroup( array(
+ 'group_id' => '__orphan__',
+ 'group_name' => 'Staff',
+ ) );
+ }
+ }
+
+ if ( !isset( $info['leader_position'] ) )
+ {
+ $info['leader_position'] = 1;
+ }
+
+ $obj = new \IPS\core\StaffDirectory\User;
+ $obj->type = $info['leader_type'];
+ $obj->type_id = $info['leader_type_id'];
+ $obj->group_id = $info['leader_group_id'];
+ $obj->position = $info['leader_position'];
+ $obj->save();
+
+ $this->software->app->addLink( $obj->id, $info['leader_id'], 'core_leaders' );
+ return $obj->id;
+ }
+
+ /**
+ * Convert a Staff Directory Group
+ *
+ * @pararm array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Staff Directory Group, or FALSE on failure.
+ */
+ public function convertLeaderGroup( $info=array() )
+ {
+ if ( !isset( $info['group_id'] ) )
+ {
+ $this->software->app->log( 'leader_group_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['group_name'] ) )
+ {
+ $this->software->app->log( 'leader_group_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $info['group_name'] = 'Untitled Group';
+ }
+
+ if ( !isset( $info['group_template'] ) )
+ {
+ $info['group_template'] = 'layout_full';
+ }
+
+ if ( !isset( $info['group_position'] ) )
+ {
+ $info['group_position'] = 1;
+ }
+
+ $obj = new \IPS\core\StaffDirectory\Group;
+ $obj->name = $info['group_name'];
+ $obj->template = $info['group_template'];
+ $obj->position = $info['group_position'];
+ $obj->save();
+
+ $this->software->app->addLink( $obj->id, $info['group_id'], 'core_leader_groups' );
+ return $obj->id;
+ }
+
+ /**
+ * Convert a Member Rank
+ *
+ * @param array $info Data to insert
+ * @param string|NULL $iconpath Icon File Path, or NULL
+ * @param string|NULL $icondata Icon File Data, or NULL
+ * @return boolean|integer The ID of the newly inserted Rank, or FALSE on failure.
+ */
+ public function convertRank( $info=array(), $iconpath=NULL, $icondata=NULL )
+ {
+ /* We don't really need an ID for these, so if one isn't specified, then that's okay. */
+ $hasId = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $hasId = FALSE;
+ $this->software->app->log( 'rank_missing_ids', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ }
+
+ /* We do need this, though. */
+ if ( !isset( $info['title'] ) )
+ {
+ $this->software->app->log( 'rank_missing_title', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ return FALSE;
+ }
+
+ if ( !isset( $info['posts'] ) )
+ {
+ $info['posts'] = 0;
+ }
+
+ if ( !isset( $info['pips'] ) )
+ {
+ $info['pips'] = 0;
+ }
+
+ if ( isset( $info['icon'] ) AND ( !is_null( $iconpath ) OR !is_null( $icondata ) ) )
+ {
+ try
+ {
+ if ( is_null( $icondata ) AND !is_null( $iconpath ) )
+ {
+ $icondata = file_get_contents( $iconpath );
+ }
+ $file = \IPS\File::create( 'core_Theme', $info['icon'], $icondata );
+ $info['icon'] = (string) $file;
+ $info['use_icon'] = 1;
+ }
+ catch( \Exception $e )
+ {
+ $info['icon'] = '';
+ $info['use_icon'] = 0;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['icon'] = '';
+ $info['use_icon'] = 0;
+ }
+ }
+ else
+ {
+ $info['icon'] = '';
+ $info['use_icon'] = 0;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['id'];
+ unset( $info['id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_member_ranks', $info );
+ \IPS\Lang::saveCustom( 'core', "core_member_rank_{$inserted_id}", $info['title'] );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_member_ranks' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Status Reply
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Status Reply, or FALSE on failure.
+ */
+ public function convertStatusReply( $info=array() )
+ {
+ if ( !isset( $info['reply_id'] ) )
+ {
+ $this->software->app->log( 'status_reply_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['reply_status_id'] ) )
+ {
+ try
+ {
+ $info['reply_status_id'] = $this->software->app->getLink( $info['reply_status_id'], 'core_member_status_updates' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'status_reply_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['reply_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'status_reply_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['reply_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['reply_member_id'] ) )
+ {
+ try
+ {
+ $info['reply_member_id'] = $this->software->app->getLink( $info['reply_member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'status_reply_no_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['reply_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'status_reply_no_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['reply_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['reply_date'] ) )
+ {
+ if ( $info['reply_date'] instanceof \IPS\DateTime )
+ {
+ $info['reply_date'] = $info['reply_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['reply_date'] = time();
+ }
+
+ if ( empty( $info['reply_content'] ) )
+ {
+ $this->software->app->log( 'status_reply_no_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['reply_id'] );
+ return FALSe;
+ }
+
+ if ( !isset( $info['reply_approved'] ) )
+ {
+ $info['reply_approved'] = 1;
+ }
+
+ if ( !isset( $info['reply_ip_address'] ) OR filter_var( $info['reply_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['reply_ip_address'] = '127.0.0.1';
+ }
+
+ $id = $info['reply_id'];
+ unset( $info['reply_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_member_status_replies', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_member_status_replies' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Status Update
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted Status Update, or FALSE on failure.
+ */
+ public function convertStatus( $info=array() )
+ {
+ if ( !isset( $info['status_id'] ) )
+ {
+ $this->software->app->log( 'status_update_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['status_member_id'] ) )
+ {
+ try
+ {
+ $info['status_member_id'] = $this->software->app->getLink( $info['status_member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'status_update_no_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['status_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'status_update_no_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['status_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['status_date'] ) )
+ {
+ if ( $info['status_date'] instanceof \IPS\DateTime )
+ {
+ $info['status_date'] = $info['status_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['status_date'] = time();
+ }
+
+ if ( empty( $info['status_content'] ) )
+ {
+ $this->software->app->log( 'status_update_no_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['status_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['status_replies'] ) )
+ {
+ /* Converter will indicate that these need recounted */
+ $info['status_replies'] = 0;
+ }
+
+ /* No longer used */
+ $info['status_last_ids'] = NULL;
+ $info['status_is_latest'] = 0;
+ $info['status_hash'] = '';
+
+ if ( !isset( $info['status_is_locked'] ) )
+ {
+ $info['status_is_locked'] = 0;
+ }
+
+ if ( !isset( $info['status_imported'] ) )
+ {
+ $info['status_imported'] = 0;
+ }
+
+ if ( isset( $info['status_author_id'] ) )
+ {
+ try
+ {
+ $info['status_author_id'] = $this->software->app->getLink( $info['status_author_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'status_update_no_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['status_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'status_update_no_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['status_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['status_author_ip'] ) OR filter_var( $info['status_author_ip'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['status_author_ip'] = '127.0.0.1';
+ }
+
+ if ( !isset( $info['status_approved'] ) )
+ {
+ $info['status_approved'] = 1;
+ }
+
+ $id = $info['status_id'];
+ unset( $info['status_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_member_status_updates', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_member_status_updates' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Member
+ *
+ * @param array $info Data to insert
+ * @param NULL|string $profilePhotoName Filename for the users Profile Photo or NULL if none set.
+ * @param NULL|string $profilePhotoPath Path to Profile Photos, or NULL to load from the database.
+ * @param NULL|string $profileFileData If loading from the database, the filedata for the profile photo from the Binary column.
+ * @param NULL|string $coverPhotoName Filename for the users Cover Photo or NULL if none set.
+ * @param NULL|string $coverPhotoPath Path to Cover Photos, or NULL to load from the database.
+ * @param NULL|string $coverFileData If loading from the database, the filedatafor the cover photo from the Binary column.
+ * @return boolean|integer The ID of the newly inserted member, or FALSE on failure.
+ * @todo Work out if we can convert Social Login Stuff.
+ * @todo Password Stuff
+ */
+ public function convertMember( $info=array(), $profileFields=array(), $profilePhotoName=NULL, $profilePhotoPath=NULL, $profileFileData=NULL, $coverPhotoName=NULL, $coverPhotoPath=NULL, $coverFileData=NULL )
+ {
+ if ( !isset( $info['member_id'] ) )
+ {
+ $this->software->app->log( 'member_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['email'] ) OR filter_var( $info['email'], FILTER_VALIDATE_EMAIL ) === FALSE )
+ {
+ /* No email, or it is invalid */
+ $newEmail = time() . '@example.com';
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_email_invalid' ), $info['email'], $newEmail ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $info['email'] = $newEmail;
+ }
+ else
+ {
+ /* If this email is already in use, we need to merge the two members. Queue Tasks later will handle synchronize stuffs */
+ $memberToTest = \IPS\Member::load( $info['email'], 'email' );
+
+ if ( $memberToTest->member_id )
+ {
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_exists' ), $info['name'], $memberToTest->name ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $this->software->app->addLink( $memberToTest->member_id, $info['member_id'], 'core_members', TRUE );
+ return $memberToTest->member_id;
+ }
+ }
+
+ /* This is required, but we can generate a default value from the Member ID */
+ if ( !isset( $info['name'] ) )
+ {
+ $info['name'] = "User_{$info['member_id']}";
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_missing_name' ), $info['name'] ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ }
+ else
+ {
+ /* If the username is longer than our defined length, then cut it down to fit rather than not converting */
+ if ( mb_strlen( $info['name'] ) > \IPS\Settings::i()->max_user_name_length )
+ {
+ $newName = mb_substr( $info['name'], 0, \IPS\Settings::i()->max_user_name_length );
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_name_too_long' ), $info['name'], $newName ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $info['name'] = $newName;
+ }
+
+ /* Is it using any blocked characters? */
+ if ( \IPS\Settings::i()->username_characters AND !preg_match( '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/i', $info['name'] ) )
+ {
+ $newName = preg_replace( '/^[' . str_replace( '\-', '-', preg_quote( \IPS\Settings::i()->username_characters, '/' ) ) . ']*$/iu', '', $info['name'] );
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_name_invalid_chars' ), $info['name'], $newName ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $info['name'] = $newName;
+ }
+
+ /* Finally, check it's not in use */
+ $memberToTest = \IPS\Member::load( $info['name'], 'name' );
+
+ if ( $memberToTest->member_id )
+ {
+ /* It is... add a tiemstamp on the end */
+ $newName = $info['name'] . time();
+ $this->software->app->log( sprintf( \IPS\Member::loggedIn()->language()->get( 'member_name_in_use' ), $info['name'], $newName ), __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $info['name'] = $newName;
+ }
+
+ unset( $memberToTest );
+ }
+
+ /* Work out passwords */
+ if ( isset( $info['password'] ) )
+ {
+ /* This is a password that is still hashed per the source */
+ $info['conv_password'] = $info['password'];
+ unset( $info['password'] );
+
+ if ( isset( $info['password_extra'] ) ) # salts and such
+ {
+ $info['conv_password_extra'] = $info['password_extra'];
+ unset( $info['password_extra'] );
+ }
+ }
+ else if ( isset( $info['md5_password'] ) )
+ {
+ /* This is a simple md5 hashed password - we can convert this to the 3.x format which will be picked up by the internal login handler later and converted */
+ $salt = '';
+
+ for ( $i = 0; $i < 5; $i++ )
+ {
+ $num = mt_rand(33, 126);
+
+ if ( $num == '92' )
+ {
+ $num = 93;
+ }
+
+ $salt .= chr( $num );
+ }
+ $info['members_pass_hash'] = md5( md5( $salt ) . $info['md5_password'] );
+ $info['members_pass_salt'] = $salt;
+ unset( $info['md5_password'] );
+ }
+ else if ( isset( $info['plain_password'] ) )
+ {
+ /* This is just a plaintext password, so we can just store it normally... I haven't decided if I really want to do this here or later as its intentionally slow */
+ $salt = ''; # generateSalt() is not a static method in \IPS\Member
+ for ( $i=0; $i<22; $i++ )
+ {
+ do
+ {
+ $chr = rand( 48, 122 );
+ }
+ while ( in_array( $chr, range( 58, 64 ) ) or in_array( $chr, range( 91, 96 ) ) );
+
+ $salt .= chr( $chr );
+ }
+ $info['members_pass_hash'] = crypt( $info['plain_password'], '$2a$13$' . $salt );
+ $info['members_pass_salt'] = $salt;
+ unset( $info['plain_password'] );
+ }
+ else
+ {
+ /* No need to do anything if we're coming from IPS4 */
+ if ( !isset( $info['members_pass_hash'] ) OR !isset( $info['members_pass_salt'] ) )
+ {
+ $info['members_pass_hash'] = NULL;
+ $info['members_pass_salt'] = NULL;
+ }
+ }
+
+ $group = \IPS\Settings::i()->member_group;
+
+ if ( isset( $info['ips_group_id'] ) )
+ {
+ $group = $info['ips_group_id'];
+ unset( $info['ips_group_id'] );
+ }
+ elseif ( isset( $info['member_group_id'] ) )
+ {
+ try
+ {
+ $group = $this->software->app->getLink( $info['member_group_id'], 'core_groups' );
+ }
+ catch( \OutOfRangeException $e ) {}
+ }
+
+ $info['member_group_id'] = $group;
+
+ /* If no join date specified, just use current time */
+ if ( isset( $info['joined'] ) )
+ {
+ if ( $info['joined'] instanceof \IPS\DateTime )
+ {
+ $info['joined'] = $info['joined']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['joined'] = time();
+ }
+
+ /* If no IP Address specified, just use a generic value */
+ if ( !isset( $info['ip_address'] ) OR filter_var( $info['ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['ip_address'] = '127.0.0.1';
+ }
+
+ /* We cannot convert themes - make sure this value is NULL */
+ $info['skin'] = NULL;
+
+ if ( !isset( $info['warn_level'] ) )
+ {
+ $info['warn_level'] = NULL;
+ }
+
+ if ( isset( $info['warn_lastwarn'] ) )
+ {
+ if ( $info['warn_lastwarn'] instanceof \IPS\DateTime )
+ {
+ $info['warn_lastwarn'] = $info['warn_lastwarn']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['warn_lastwarn'] = 0;
+ }
+
+ /* Cannot convert languages - set to default */
+ $info['language'] = \IPS\Lang::defaultLanguage();
+
+ if ( isset( $info['restrict_post'] ) )
+ {
+ if ( $info['restrict_post'] instanceof \IPS\DateTime )
+ {
+ $info['restrict_post'] = $info['restrict_post']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['restrict_post'] = 0;
+ }
+
+ if ( !isset( $info['bday_day'] ) )
+ {
+ $info['bday_day'] = NULL;
+ }
+
+ if ( !isset( $info['bday_month'] ) )
+ {
+ $info['bday_month'] = NULL;
+ }
+
+ if ( !isset( $info['bday_year'] ) )
+ {
+ $info['bday_year'] = NULL;
+ }
+
+ if ( !isset( $info['msg_count_new'] ) )
+ {
+ $info['msg_count_new'] = 0;
+ }
+
+ if ( !isset( $info['msg_count_total'] ) )
+ {
+ $info['msg_count_total'] = 0;
+ }
+
+ if ( !isset( $info['msg_count_reset'] ) )
+ {
+ $info['msg_count_reset'] = 0;
+ }
+
+ if ( !isset( $info['msg_show_notification'] ) )
+ {
+ $info['msg_show_notification'] = 0;
+ }
+
+ if ( isset( $info['last_visit'] ) )
+ {
+ if ( $info['last_visit'] instanceof \IPS\DateTime )
+ {
+ $info['last_visit'] = $info['last_visit']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_visit'] = time();
+ }
+
+ if ( isset( $info['last_activity'] ) )
+ {
+ if ( $info['last_activity'] instanceof \IPS\DateTime )
+ {
+ $info['last_activity'] = $info['last_activity']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_activity'] = time();
+ }
+
+ if ( isset( $info['mod_posts'] ) )
+ {
+ if ( $info['mod_posts'] instanceof \IPS\DateTime )
+ {
+ $info['mod_posts'] = $info['mod_posts']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['mod_posts'] = 0;
+ }
+
+ if ( isset( $info['auto_track'] ) )
+ {
+ $autoTrack = array( 'content' => 0, 'comments' => 0, 'method' => 'none' );
+
+ /* Is it JSON or an array? */
+ if ( !is_array( $info['auto_track'] ) AND $autoTrackJson = @json_decode( $info['auto_track'], TRUE ) )
+ {
+ $info['auto_track'] = $autoTrackJson;
+ }
+
+ if ( is_array( $info['auto_track'] ) )
+ {
+ /* Make sure everything is valid */
+ if ( isset( $info['auto_track']['content'] ) )
+ {
+ $autoTrack['content'] = intval( $info['auto_track']['content'] );
+ }
+
+ if ( isset( $info['auto_track']['comments'] ) )
+ {
+ $autoTrack['comments'] = intval( $info['auto_track']['comments'] );
+ }
+
+ if ( isset( $info['auto_track']['method'] ) AND in_array( $info['auto_track']['method'], array( 'immediate', 'daily', 'weekly' ) ) )
+ {
+ $autoTrack['method'] = $info['auto_track']['method'];
+ }
+ }
+
+ /* Encode and store */
+ $info['auto_track'] = json_encode( $autoTrack );
+ }
+ else
+ {
+ /* Interestingly, this is 0 by default */
+ $info['auto_track'] = 0;
+ }
+
+ if ( isset( $info['temp_ban'] ) )
+ {
+ if ( $info['temp_ban'] instanceof \IPS\DateTime )
+ {
+ $info['temp_ban'] = $info['temp_ban']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['temp_ban'] = 0;
+ }
+
+ if ( isset( $info['mgroup_others'] ) AND !empty( $info['mgroup_others'] ) )
+ {
+ $newGroups = array();
+ /* Just one? */
+ if ( is_numeric( $info['mgroup_others'] ) )
+ {
+ try
+ {
+ $newGroups[] = $this->software->app->getLink( $info['mgroup_others'], 'core_groups' );
+ }
+ catch( \OutOfRangeException $e ) {}
+ }
+ /* An array? */
+ else if ( is_array( $info['mgroup_others'] ) )
+ {
+ if ( count( $info['mgroup_others'] ) )
+ {
+ foreach( $info['mgroup_others'] AS $group )
+ {
+ try
+ {
+ $newGroups[] = $this->software->app->getLink( $group, 'core_groups' );
+ }
+ catch( \OutOfRangeException $e ) {}
+ }
+ }
+ }
+ /* Comma delimited list? */
+ else if ( mb_strstr( $info['mgroup_others'], ',' ) )
+ {
+ $groups = explode( ',', $info['mgroup_others'] );
+ if ( count( $groups ) )
+ {
+ foreach( $groups AS $group )
+ {
+ try
+ {
+ $newGroups[] = $this->software->app->getLink( $group, 'core_groups' );
+ }
+ catch( \OutOfRangeException $e ) {}
+ }
+ }
+ }
+
+ if ( count( $newGroups ) )
+ {
+ $info['mgroup_others'] = implode( ',', $newGroups );
+ }
+ else
+ {
+ $info['mgroup_others'] = '';
+ }
+ }
+ else
+ {
+ $info['mgroup_others'] = '';
+ }
+
+ /* Some generic stuff for uniformity */
+ $info['member_login_key'] = md5( uniqid() );
+ $info['member_login_key_expire'] = \IPS\DateTime::create()->add( new \DateInterval( 'P7D' ) )->getTimestamp();
+ $info['members_seo_name'] = \IPS\Http\Url::seoTitle( $info['name'] );
+ $info['members_cache'] = NULL;
+ $info['failed_logins'] = json_encode( array() );
+ $info['failed_login_count'] = 0;
+
+ if ( !isset( $info['members_profile_views'] ) )
+ {
+ $info['members_profile_views'] = 0;
+ }
+
+ $bitoptions = 0;
+ if ( isset( $info['members_bitoptions'] ) AND is_array( $info['members_bitoptions'] ) )
+ {
+ foreach( \IPS\Member::$bitOptions['members_bitoptions']['members_bitoptions'] AS $key => $value )
+ {
+ if ( isset( $info['members_bitoptions'][$key] ) AND $info['members_bitoptions'][$key] == TRUE )
+ {
+ $bitoptions += $value;
+ }
+ }
+ }
+ $info['members_bitoptions'] = $bitoptions;
+
+ $bitoptions2 = 0;
+ if ( isset( $info['members_bitoptions2'] ) AND is_array( $info['members_bitoptions2'] ) )
+ {
+ foreach( \IPS\Member::$bitOptions['members_bitoptions']['members_bitoptions2'] AS $key => $value )
+ {
+ if ( isset( $info['members_bitoptions2'][$key] ) AND $info['members_bitoptions2'] == TRUE )
+ {
+ $bitoptions2 += $value;
+ }
+ }
+ }
+ $info['members_bitoptions2'] = $bitoptions2;
+
+ /* These are no longer used */
+ $info['fb_bwoptions'] = 0;
+ $info['tc_bwoptions'] = 0;
+
+ /* We won't know these */
+ $info['members_day_posts'] = '0,0';
+ $info['notification_cnt'] = 0;
+ $info['ipsconnect_id'] = 0;
+
+ if ( isset( $info['pp_last_visitors'] ) )
+ {
+ if ( !is_array( $info['pp_last_visitors'] ) )
+ {
+ $info['pp_last_visitors'] = @json_decode( $info['pp_last_visitors'], TRUE );
+ }
+
+ if ( count( $info['pp_last_visitors'] ) )
+ {
+ $newVisitors = array();
+ $count = 0;
+ foreach( $info['pp_last_visitors'] AS $memberId => $date )
+ {
+ try
+ {
+ if ( $count > 5 )
+ {
+ throw new \OutOfRangeException;
+ }
+ $newVisitorId = $this->software->app->getLink( $memberId, 'core_members' )->first();
+
+ if ( $date instanceof \IPS\DateTime )
+ {
+ $date = $date->getTimestamp();
+ }
+
+ $newVisitors[$newVisitorId] = $date;
+
+ $count++;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+
+ if ( count( $newVisitors ) )
+ {
+ $info['pp_last_visitors'] = json_encode( $newVisitors );
+ }
+ }
+ else
+ {
+ $info['pp_last_visitors'] = NULL;
+ }
+ }
+ else
+ {
+ $info['pp_last_visitors'] = NULL;
+ }
+
+ /* We need to make sure this is not set as we need it to be something specific later */
+ unset( $info['pp_photo_type'], $info['pp_main_photo'], $info['pp_thumb_photo'] );
+
+ /* Profile Photos! */
+ if ( !is_null( $profilePhotoName ) AND ( !is_null( $profilePhotoPath ) OR !is_null( $profileFileData ) ) )
+ {
+ /* <3 IPS4 */
+ try
+ {
+ if ( is_null( $profileFileData ) AND !is_null( $profilePhotoPath ) )
+ {
+ $profileFileData = file_get_contents( rtrim( $profilePhotoPath, '/' ) . '/' . $profilePhotoName );
+ }
+
+ $photo = \IPS\File::create( 'core_Profile', $profilePhotoName, $profileFileData );
+ $info['pp_photo_type'] = 'custom';
+ $info['pp_main_photo'] = (string) $photo;
+ $info['pp_thumb_photo'] = (string) $photo->thumbnail( 'core_Profile', \IPS\PHOTO_THUMBNAIL_SIZE, \IPS\PHOTO_THUMBNAIL_SIZE );
+ }
+ catch( \Exception $e )
+ {
+ $info['pp_photo_type'] = '';
+ $info['pp_main_photo'] = NULL;
+ $info['pp_thumb_photo'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['pp_photo_type'] = '';
+ $info['pp_main_photo'] = NULL;
+ $info['pp_thumb_photo'] = NULL;
+ }
+ }
+ else
+ {
+ $info['pp_photo_type'] = '';
+ $info['pp_main_photo'] = NULL;
+ $info['pp_thumb_photo'] = NULL;
+ }
+
+ /* Cover Photos! */
+ if ( !is_null( $coverPhotoName ) AND ( !is_null( $coverPhotoPath ) OR !is_null( $coverFileData ) ) )
+ {
+ try
+ {
+ if ( is_null( $coverFileData ) AND !is_null( $coverPhotoPath ) )
+ {
+ $coverFileData = file_get_contents( rtrim( $coverPhotoPath, '/' ) .'/' . $coverPhotoName );
+ }
+ $cover = \IPS\File::create( 'core_Profile', $coverPhotoName, $coverFileData );
+ $info['pp_cover_photo'] = (string) $cover;
+ $info['pp_cover_offset'] = 0; /* Designs are different so this will never likely be correct */
+ }
+ catch( \Exception $e )
+ {
+ $info['pp_cover_photo'] = '';
+ $info['pp_cover_offset'] = 0;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['pp_cover_photo'] = '';
+ $info['pp_cover_offset'] = 0;
+ }
+ }
+
+ if ( empty( $info['pp_photo_type'] ) AND isset( $info['pp_gravatar'] ) )
+ {
+ /* Make sure email is valid */
+ if ( filter_var( $info['pp_gravatar'], FILTER_VALIDATE_EMAIL ) !== FALSE )
+ {
+ $info['pp_photo_type'] = 'gravatar';
+ if ( $info['pp_gravatar'] == $info['email'] )
+ {
+ $info['pp_gravatar'] = NULL;
+ }
+ }
+ else
+ {
+ $info['pp_photo_type'] = '';
+ $info['pp_gravatar'] = NULL;
+ }
+ }
+
+ if ( !isset( $info['pp_setting_count_comments'] ) )
+ {
+ $info['pp_setting_count_comments'] = 0;
+ }
+
+ if ( !isset( $info['pp_reputation_points'] ) )
+ {
+ $info['pp_reputation_points'] = 0;
+ }
+
+ if ( !isset( $info['signature'] ) )
+ {
+ $info['signature'] = NULL;
+ }
+
+ if ( isset( $info['pconversation_filters'] ) )
+ {
+ /* We don't really need to do much here - just if it's an array, encode it */
+ if ( is_array( $info['pconversation_filters'] ) )
+ {
+ $info['pconversation_filters'] = json_encode( $info['pconversation_filters'] );
+ }
+ }
+ else
+ {
+ $info['pconversation_filters'] = NULL;
+ }
+
+ /* No Longer Used */
+ $info['pp_customization'] = NULL;
+
+ if ( isset( $info['timezone'] ) )
+ {
+ if ( $info['timezone'] instanceof \DateTimeZone )
+ {
+ $info['timezone'] = $info['timezone']->getName();
+ }
+ else
+ {
+ if ( ( $info['timezone'] = @timezone_name_from_abbr( NULL, $info['timezone'] ) ) === FALSE )
+ {
+ $info['timezone'] = 'UTC';
+ }
+ }
+ }
+ else
+ {
+ $info['timezone'] = 'UTC';
+ }
+
+ if ( isset( $info['allow_admin_mails'] ) )
+ {
+ $info['allow_admin_mails'] = (boolean) $info['allow_admin_mails'];
+ }
+ else
+ {
+ $info['allow_admin_mails'] = FALSE;
+ }
+
+ $info['ipsconnect_revalidate_url'] = NULL;
+
+ if ( !isset( $info['members_disable_pm'] ) )
+ {
+ $info['members_disable_pm'] = 0;
+ }
+
+ $info['marked_site_read'] = time();
+ $info['acp_skin'] = NULL;
+ $info['acp_language'] = \IPS\Lang::defaultLanguage();
+
+ if ( !isset( $info['member_title'] ) )
+ {
+ $info['member_title'] = NULL;
+ }
+
+ /* Check length of supplied title */
+ if( $info['member_title'] AND ( mb_strlen( $info['member_title'] ) > 64 ) )
+ {
+ $this->software->app->log( 'members_title_truncated', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['member_id'] );
+ $info['member_title'] = mb_substr( $info['member_title'], 0, 64 );
+ }
+
+ if ( !isset( $info['member_posts'] ) )
+ {
+ $info['member_posts'] = 0;
+ }
+
+ if ( isset( $info['member_last_post'] ) )
+ {
+ if ( $info['member_last_post'] instanceof \IPS\DateTime )
+ {
+ $info['member_last_post'] = $info['member_last_post']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['member_last_post'] = NULL;
+ }
+
+ $info['member_streams'] = NULL;
+ $info['create_menu'] = NULL;
+ $id = $info['member_id'];
+ unset( $info['member_id'] );
+
+ /* Whew, finally */
+ $inserted_id = \IPS\Db::i()->insert( 'core_members', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_members' );
+ \IPS\Db::i()->replace( 'core_pfields_content', $this->_formatMemberProfileFieldContent( $inserted_id, $profileFields ), TRUE );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Warning Action
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted warning action, or FALSE on failure.
+ */
+ public function convertWarnAction( $info=array() )
+ {
+ /* We do not really need an ID here */
+ $hasId = TRUE;
+ if ( !isset( $info['wa_id'] ) )
+ {
+ $this->software->app->log( 'wa_missing_ids', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ $hasId = FALSE;
+ }
+
+ if ( !isset( $info['wa_points'] ) )
+ {
+ $info['wa_points'] = 0;
+ }
+
+ if ( !isset( $info['wa_mq'] ) )
+ {
+ $info['wa_mq'] = 0;
+ }
+
+ if ( !isset( $info['wa_mq_unit'] ) OR !in_array( $info['wa_mq_unit'], array( 'd', 'h' ) ) )
+ {
+ $info['wa_mq_unit'] = 'h';
+ }
+
+ if ( !isset( $info['wa_rpa'] ) )
+ {
+ $info['wa_rpa'] = 0;
+ }
+
+ if ( !isset( $info['wa_rpa_unit'] ) OR !in_array( $info['wa_rpa_unit'], array( 'd', 'h' ) ) )
+ {
+ $info['wa_rpa_unit'] = 'h';
+ }
+
+ if ( !isset( $info['wa_suspend'] ) )
+ {
+ $info['wa_suspend'] = 0;
+ }
+
+ if ( !isset( $info['wa_suspend_unit'] ) OR !in_array( $info['wa_rpa_unit'], array( 'd', 'h' ) ) )
+ {
+ $info['wa_suspend_unit'] = 'h';
+ }
+
+ if ( !isset( $info['wa_override'] ) )
+ {
+ $info['wa_override'] = 0;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['wa_id'];
+ unset( $info['wa_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_members_warn_actions', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_members_warn-actions' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Warning Log
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted warning log, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertWarnLog( $info=array() )
+ {
+ if ( !isset( $info['wl_id'] ) )
+ {
+ $this->software->app->log( 'wl_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['wl_member'] ) )
+ {
+ try
+ {
+ $info['wl_member'] = $this->software->app->getLink( $info['wl_member'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'wl_member_missing', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['wl_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'wl_member_missing', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['wl_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['wl_moderator'] ) )
+ {
+ try
+ {
+ $info['wl_moderator'] = $this->software->app->getLink( $info['wl_moderator'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['wl_moderator'] = 0;
+ }
+ }
+ else
+ {
+ $info['wl_moderator'] = 0;
+ }
+
+ if ( isset( $info['wl_date'] ) )
+ {
+ if ( $info['wl_date'] instanceof \IPS\DateTime )
+ {
+ $info['wl_date'] = $info['wl_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['wl_date'] = time();
+ }
+
+ if ( isset( $info['wl_reason'] ) )
+ {
+ try
+ {
+ $info['wl_reason'] = $this->software->app->getLink( $info['wl_reason'], 'core_members_warn_reasons' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['wl_reason'] = 0;
+ }
+ }
+ else
+ {
+ $info['wl_reason'] = 0;
+ }
+
+ if ( !isset( $info['wl_points'] ) )
+ {
+ $info['wl_points'] = 0;
+ }
+
+ if ( !isset( $info['wl_note_member'] ) )
+ {
+ $info['wl_note_member'] = '';
+ }
+
+ if ( !isset( $info['wl_note_mods'] ) )
+ {
+ $info['wl_note_mods'] = '';
+ }
+
+ foreach( array( 'wl_mq', 'wl_rpa', 'wl_suspend' ) AS $restriction )
+ {
+ if ( isset( $info[$restriction] ) )
+ {
+ if ( $info[$restriction] instanceof \IPS\DateTime )
+ {
+ /* Naughty copy/paste */
+ $difference = \IPS\DateTime::create()->diff( $info[$restriction] );
+ $period = 'P';
+ foreach ( array( 'y' => 'Y', 'm' => 'M', 'd' => 'D' ) as $k => $v )
+ {
+ if ( $difference->$k )
+ {
+ $period .= $difference->$k . $v;
+ }
+ }
+ $time = '';
+ foreach ( array( 'h' => 'H', 'i' => 'M', 's' => 'S' ) as $k => $v )
+ {
+ if ( $difference->$k )
+ {
+ $time .= $difference->$k . $v;
+ }
+ }
+ if ( $time )
+ {
+ $period .= 'T' . $time;
+ }
+
+ $info[$restriction] = $period;
+ }
+ }
+ else
+ {
+ $info[$restriction] = NULL;
+ }
+ }
+
+ if ( !isset( $info['wl_acknowledged'] ) )
+ {
+ /* Normally we would use the default, but in this instance let's assume the warning was acknowledged */
+ $info['wl_acknowledged'] = 1;
+ }
+
+ /* As far as where the content is stored... we can't figure that out automatically - the converter will need to do so, and pass the appropriate values and ID's for app and content */
+ if ( !isset( $info['wl_content_app'] ) )
+ {
+ $info['wl_content_app'] = NULL;
+ }
+
+ if ( !isset( $info['wl_content_id1'] ) )
+ {
+ $info['wl_content_id1'] = NULL;
+ }
+
+ if ( !isset( $info['wl_content_id2'] ) )
+ {
+ $info['wl_content_id2'] = NULL;
+ }
+
+ if ( !isset( $info['wl_content_module'] ) )
+ {
+ $info['wl_content_module'] = NULL;
+ }
+
+ if ( isset( $info['wl_expire_date'] ) )
+ {
+ if ( $info['wl_expire_date'] instanceof \IPS\DateTime )
+ {
+ $info['wl_expire_date'] = $info['wl_expire_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['wl_expire_date'] = -1;
+ }
+
+ $id = $info['wl_id'];
+ unset( $info['wl_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_members_warn_logs', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_members_warn_logs' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Warning Reason
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted warning reason, or FALSE on failure.
+ */
+ public function convertWarnReason( $info=array() )
+ {
+ if ( !isset( $info['wr_id'] ) )
+ {
+ $this->software->app->log( 'wr_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['wr_name'] ) )
+ {
+ $info['wr_name'] = "Untitled Reason {$info['wr_id']}";
+ $this->software->app->log( 'wr_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['wr_id'] );
+ }
+
+ if ( !isset( $info['wr_points'] ) )
+ {
+ $info['wr_points'] = 0;
+ }
+
+ if ( !isset( $info['wr_points_override'] ) )
+ {
+ $info['wr_points_override'] = 0;
+ }
+
+ if ( !isset( $info['wr_remove'] ) )
+ {
+ $info['wr_remove'] = 0;
+ }
+
+ if ( !isset( $info['wr_remove_unit'] ) OR !in_array( $info['wr_remove_unit'], array( 'h', 'd' ) ) )
+ {
+ $info['wr_remove_unit'] = 'h';
+ }
+
+ if ( !isset( $info['we_remove_override'] ) )
+ {
+ $info['wr_remove_override'] = 0;
+ }
+
+ if ( !isset( $info['wr_order'] ) )
+ {
+ $highest = \IPS\Db::i()->select( 'MAX(wr_order)', 'core_members_warn_reasons' )->first();
+ $info['wr_order'] = $highest + 1;
+ }
+
+ $id = $info['wr_id'];
+ unset( $info['wr_id'] );
+
+ $name = $info['wr_name'];
+ unset( $info['wr_name'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_members_warn_reasons', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_members_warn_reasons' );
+
+ \IPS\Lang::saveCustom( 'core', "core_warn_reason_{$inserted_id}", $name );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Private Message
+ *
+ * @param array $topic The Message Topic Data to insert
+ * @param array $maps The User Map Data to insert. Example: array( memberId => array( data ) )
+ * @return boolean|integer The ID of the newly inserted Message Topic, or FALSE on failure.
+ */
+ public function convertPrivateMessage( $topic=array(), $maps=array() )
+ {
+ if ( !isset( $topic['mt_id'] ) )
+ {
+ $this->software->app->log( 'private_message_topic_ids_missing', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( count( $maps ) == 0 )
+ {
+ $this->software->app->log( 'private_message_topic_no_users', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $topic['mt_date'] ) )
+ {
+ if ( $topic['mt_date'] instanceof \IPS\DateTime )
+ {
+ $topic['mt_date'] = $topic['mt_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ /* Converter will indicate PM's need rebuilding if any information is missing */
+ $topic['mt_date'] = 0;
+ }
+
+ if ( !isset( $topic['mt_title'] ) )
+ {
+ $topic['mt_title'] = "Untitled Conversation {$topic['mt_id']}";
+ $this->software->app->log( 'private_message_topic_title_missing', __METHOD__, \IPS\convert\App::LOG_NOTICE, $topic['mt_id'] );
+ }
+
+ /* No Longer Used */
+ $topic['mt_hasattach'] = 0;
+ $topic['mt_to_member_id'] = 0;
+ $topic['mt_is_draft'] = 0;
+ $topic['mt_is_system'] = 0;
+
+ if ( isset( $topic['mt_starter_id'] ) )
+ {
+ try
+ {
+ $topic['mt_starter_id'] = $this->software->app->getLink( $topic['mt_starter_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'private_message_topic_starter_missing', __METHOD__, \IPS\convert\App::LOG_WARNING, $topic['mt_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'private_message_topic_starter_missing', __METHOD__, \IPS\convert\App::LOG_WARNING, $topic['mt_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $topic['mt_start_time'] ) )
+ {
+ if ( $topic['mt_start_time'] instanceof \IPS\DateTime )
+ {
+ $topic['mt_start_time'] = $topic['mt_start_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ /* Assign the value of mt_date - if it's 0 we'll rebuild anyway */
+ $topic['mt_start_time'] = $topic['mt_date'];
+ }
+
+ if ( isset( $topic['mt_last_post_time'] ) )
+ {
+ if ( $topic['mt_last_post_time'] instanceof \IPS\DateTime )
+ {
+ $topic['mt_last_post_time'] = $topic['mt_last_post_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $topic['mt_last_post_time'] = 0;
+ }
+
+ if ( !isset( $topic['mt_to_count'] ) )
+ {
+ $topic['mt_to_count'] = count( $maps );
+ }
+
+ if ( !isset( $topic['mt_replies'] ) )
+ {
+ $topic['mt_replies'] = 0;
+ }
+
+ $topicId = $topic['mt_id'];
+ unset( $topic['mt_id'] );
+
+ $topicInsertedId = \IPS\Db::i()->insert( 'core_message_topics', $topic );
+ $this->software->app->addLink( $topicInsertedId, $topicId, 'core_message_topics' );
+
+ /* Whew... let's do our maps now */
+ $mapsInserted = array();
+ foreach( $maps AS $memberId => $map )
+ {
+ /* We don't necessarily need an existing ID for the maps */
+ $hasMapId = TRUE;
+ if ( !isset( $map['map_id'] ) )
+ {
+ /* We don't really need to log it either - most software won't have the equivalent of this column */
+ $hasMapId = FALSE;
+ }
+
+ if ( isset( $map['map_user_id'] ) )
+ {
+ try
+ {
+ $map['map_user_id'] = $this->software->app->getLink( $map['map_user_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'private_message_map_missing_user', __METHOD__, \IPS\convert\App::LOG_WARNING, $topicId );
+ continue;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'private_message_map_missing_user', __METHOD__, \IPS\convert\App::LOG_WARNING, $topicId );
+ continue;
+ }
+
+ /* We already know this */
+ $map['map_topic_id'] = $topicInsertedId;
+
+ if ( !isset( $map['map_folder_id'] ) )
+ {
+ $map['map_folder_id'] = 'myconvo';
+ }
+
+ if ( isset( $map['map_read_time'] ) )
+ {
+ if ( $map['map_read_time'] instanceof \IPS\DateTime )
+ {
+ $map['map_read_time'] = $map['map_read_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $map['map_read_time'] = time();
+ }
+
+ if ( !isset( $map['map_user_active'] ) )
+ {
+ $map['map_user_active'] = 1;
+ }
+
+ if ( !isset( $map['map_user_banned'] ) )
+ {
+ $map['map_user_banned'] = 0;
+ }
+
+ if ( !isset( $map['map_has_unread'] ) )
+ {
+ /* We can dynamically set this if it's unknown by checking last read time */
+ if ( $map['map_read_time'] > time() )
+ {
+ $map['map_has_unread'] = 1;
+ }
+ else
+ {
+ $map['map_has_unread'] = 0;
+ }
+ }
+
+ /* Unused */
+ $map['map_is_system'] = 0;
+
+ if ( !isset( $map['map_is_starter'] ) )
+ {
+ if ( $map['map_user_id'] == $topic['mt_starter_id'] )
+ {
+ $map['map_is_starter'] = 1;
+ }
+ else
+ {
+ $map['map_is_starter'] = 0;
+ }
+ }
+
+ if ( isset( $map['map_left_time'] ) )
+ {
+ if ( $map['map_left_time'] instanceof \IPS\DateTime )
+ {
+ $map['map_left_time'] = $map['map_left_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $map['map_left_time'] = 0;
+ }
+
+ if ( !isset( $map['map_ignore_notification'] ) )
+ {
+ $map['map_ignore_notification'] = 0;
+ }
+
+ if ( isset( $map['map_last_topic_reply'] ) )
+ {
+ if ( $map['map_last_topic_reply'] instanceof \IPS\DateTime )
+ {
+ $map['map_last_topic_reply'] = $map['map_last_topic_reply']->getTimestamp();
+ }
+ }
+ else
+ {
+ $map['map_last_topic_reply'] = $topic['mt_last_post_time'];
+ }
+
+ if ( $hasMapId )
+ {
+ $mapId = $map['map_id'];
+ unset( $map['map_id'] );
+ }
+
+ try
+ {
+ /* Does a map for this user already exist? */
+ $existing = \IPS\Db::i()->select( '*', 'core_message_topic_user_map', array( "map_topic_id=? AND map_user_id=?", $map['map_topic_id'], $map['map_user_id'] ) )->first();
+ $mapInsertedId = $existing['map_id'];
+ $mapsInserted[] = $existing['map_id'];
+ }
+ catch( \UnderflowException $e )
+ {
+ $mapInsertedId = \IPS\Db::i()->insert( 'core_message_topic_user_map', $map );
+ $mapsInserted[] = $mapInsertedId;
+ }
+
+ if ( $hasMapId )
+ {
+ $this->software->app->addLink( $mapInsertedId, $mapId, 'core_message_topic_user_map' );
+ }
+ }
+
+ /* Did we actually add any maps? */
+ if ( count( $mapsInserted ) == 0 )
+ {
+ /* Nope... clean up, log, and return */
+ \IPS\Db::i()->delete( 'core_message_topics', array( "mt_id=?", $topicInsertedId ) );
+ \IPS\Db::i()->delete( 'core_message_posts', array( "msg_topic_id=?", $topicInsertedId ) );
+ \IPS\Db::i()->delete( 'convert_link_pms', array( "ipb_id=? AND type=? AND app=?", $topicInsertedId, 'core_message_topics', $this->software->app->app_id ) );
+ $this->software->app->log( 'private_message_topic_missing_maps', __METHOD__, \IPS\convert\App::LOG_WARNING, $topicId );
+ return FALSE;
+ }
+
+ /* Still here? All good - just return the new topic ID */
+ return $topicInsertedId;
+ }
+
+ /**
+ * Convert a private message reply
+ *
+ * @param array $info Data to insert
+ * @return bool|int The ID of the newly inserted reply, or FALSE on failure.
+ */
+ public function convertPrivateMessageReply( $info=array() )
+ {
+ if ( !isset( $info['msg_id'] ) )
+ {
+ $this->software->app->log( 'private_message_reply_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['msg_topic_id'] ) )
+ {
+ $this->software->app->log( 'private_message_reply_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['msg_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['msg_post'] ) )
+ {
+ $this->software->app->log( 'private_message_reply_no_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['msg_id'] );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['msg_topic_id'] = $this->software->app->getLink( $info['msg_topic_id'], 'core_message_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'private_message_reply_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['msg_id'] );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['msg_author_id'] = $this->software->app->getLink( $info['msg_author_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['msg_author_id'] = 0;
+ }
+
+ if ( isset( $info['msg_date'] ) )
+ {
+ if ( $info['msg_date'] instanceof \IPS\DateTime )
+ {
+ $info['msg_date'] = $info['msg_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['msg_date'] = time();
+ }
+
+ if ( !isset( $info['msg_is_first_post'] ) )
+ {
+ $info['msg_is_first_post'] = 0;
+ }
+
+ if ( !isset( $info['msg_ip_address'] ) OR filter_var( $info['msg_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['msg_ip_address'] = '127.0.0.1';
+ }
+
+ $info['msg_post_key'] = 0;
+
+ $id = $info['msg_id'];
+ unset( $info['msg_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_message_posts', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_message_posts' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Moderator
+ *
+ * @param array $info Data to insert
+ * @return boolean TRUE on success, or FALSE on failure.
+ */
+ public function convertModerator( $info=array() )
+ {
+ if ( !isset( $info['type'] ) OR !in_array( $info['type'], array( 'g', 'm' ) ) )
+ {
+ $this->software->app->log( 'moderator_type_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'moderator_type_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['perms'] ) )
+ {
+ $info['perms'] = '*';
+ }
+ else if ( $info['perms'] !== '*' AND is_array( $info['perms'] ) )
+ {
+ $info['perms'] = json_encode( $info['perms'] );
+ }
+
+ switch( $info['type'] )
+ {
+ case 'm':
+ $info['id'] = $this->software->app->getLink( $info['id'], 'core_members' );
+ break;
+
+ case 'g':
+ $info['id'] = $this->software->app->getLink( $info['id'], 'core_groups' );
+ break;
+ }
+
+ \IPS\Db::i()->insert( 'core_moderators', $info );
+
+ return TRUE;
+ }
+
+ /**
+ * Convert Permissions
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted permission row, or FALSE on failure.
+ * @note These should be converted when the relevant node is converted.
+ */
+ public function convertPermission( $info=array() )
+ {
+ /* Valid app? */
+ if ( !isset( $info['app'] ) )
+ {
+ $this->software->app->log( 'permission_index_app_missing', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ try
+ {
+ $application = \IPS\Application::load( $info['app'] );
+
+ if ( \IPS\Application::appIsEanbled( $info['app'] ) === FALSE )
+ {
+ throw new \UnexpectedValueException;
+ }
+ }
+ catch( \UnexpectedValueException $e )
+ {
+ $this->software->application->log( 'permission_index_app_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['perm_type'] ) )
+ {
+ $this->software->app->log( 'permission_index_missing_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ $nodeClass = NULL;
+ foreach( $application->extensions( 'core', 'Permissions' ) AS $extension )
+ {
+ foreach( array_keys( $extension->getNodeClasses() ) AS $class )
+ {
+ if ( $class::$permType == $info['perm_type'] )
+ {
+ $nodeClass = $class;
+ break 2;
+ }
+ }
+ }
+
+ if ( is_null( $nodeClass ) )
+ {
+ $this->software->app->log( 'permission_index_invalid_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['perm_type_id'] ) )
+ {
+ try
+ {
+ $info['perm_type_id'] = $this->software->app->getLink( $info['perm_type_id'], $nodeClass::$databaseTable );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'permission_index_missing_type_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'permission_index_missing_type_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Does this permission index already exist? */
+ try
+ {
+ $perm = \IPS\Db::i()->select( '*', 'core_permission_index', array( "app=? AND perm_type=? AND perm_type_id=?", $info['app'], $info['perm_type'], $info['perm_type_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* No, create a new one. This will almost always be the case. */
+ $perm = array(
+ 'app' => $info['app'],
+ 'perm_type' => $info['perm_type'],
+ 'perm_type_id' => $info['perm_type_id'],
+ 'perm_view' => NULL,
+ 'perm_2' => NULL,
+ 'perm_3' => NULL,
+ 'perm_4' => NULL,
+ 'perm_5' => NULL,
+ 'perm_6' => NULL,
+ 'perm_7' => NULL,
+ 'owner_only' => 0,
+ 'friend_only' => NULL,
+ );
+
+ $permId = \IPS\Db::i()->insert( 'core_permission_index', $perm );
+ $perm['perm_id'] = $permId;
+ }
+
+ foreach( $nodeClass::$permissionMap AS $key => $permission )
+ {
+ /* If it's the same value just use that. */
+ if ( $info["perm_{$key}"] == $perm["perm_{$permission}"] )
+ {
+ $perm["perm_{$permission}"] = $info["perm_{$key}"];
+ break;
+ }
+
+ /* If our current permission is NULL, but we are explicitly granting all, do that. */
+ if ( is_null( $perm["perm_{$permission}"] ) AND $info["perm_{$key}"] == '*' )
+ {
+ $perm["perm_{$permission}"] = '*';
+ break;
+ }
+
+ /* If our current permission is NULL, but we are assigning groups, do that. */
+ if ( is_null( $perm["perm_{$permission}"] ) AND ( is_array( $info["perm_{$key}"] ) OR mb_strstr( $info["perm_{$key}"], ',' ) OR is_numeric( $info["perm_{$key}"] ) ) )
+ {
+ if ( !is_array( $info["perm_{$key}"] ) )
+ {
+ $info["perm_{$key}"] = explode( ',', $info["perm_{$key}"] );
+ }
+
+ if ( count( $info["perm_{$key}"] ) )
+ {
+ $groupsToAdd = array();
+ foreach( $info["perm_{$key}"] AS $group )
+ {
+ try
+ {
+ $group = $this->software->app->getLink( $group, 'core_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ $groupsToAdd[] = $group;
+ }
+
+ $perm["perm_{$permission}"] = implode( ',', $groupsToAdd );
+ break;
+ }
+ }
+
+ /* If our current permission has specific groups, and we are passing more groups, merge them */
+ if ( mb_strstr( $perm["perm_{$permission}"], ',' ) OR is_numeric( $perm["perm_{$permission}"] ) )
+ {
+ $currentGroups = explode( ',', $perm["perm_{$permission}"] );
+ if ( count( $currentGroups ) )
+ {
+ $groupsToAdd = array();
+ if ( !is_array( $info["perm_{$key}"] ) )
+ {
+ $info["perm_{$key}"] = explode( ',', $info["perm_{$key}"] );
+ }
+
+ if ( count( $info["perm_{$key}"] ) )
+ {
+ foreach( $info["perm_{$key}"] AS $group )
+ {
+ try
+ {
+ $group = $this->software->app->getLink( $group, 'core_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ if ( !in_array( $currentGroups ) )
+ {
+ $groupsToAdd[] = $group;
+ }
+ }
+ }
+
+ $perm["perm_{$permission}"] = array_merge( $currentGroups, $groupsToAdd );
+ break;
+ }
+ }
+
+ /* Still here? Something went wrong... log a notice and assign NULL */
+ $perm["perm_{$permission}"] = NULL;
+ $this->software->app->log( 'permission_index_null', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['perm_type_id'] );
+ }
+
+ /* Update the database */
+ \IPS\Db::i()->update( 'core_permission_index', $perm, array( "perm_id=?", $perm['perm_id'] ) );
+ return $perm['perm_id'];
+ }
+
+ /**
+ * Convert a Profile Field Group
+ *
+ * @param array $info Data to insert
+ * @param integer|NULL $mergeWith The group we are merging with, or NULL to not merge.
+ * @return boolean|integer The ID of the newly inserted profile field group, or FALSE on failure
+ */
+ public function convertProfileFieldGroup( $info=array(), $mergeWith=NULL )
+ {
+ if ( !isset( $info['pf_group_id'] ) )
+ {
+ $this->software->app->log( 'profile_field_group_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !is_null( $mergeWith ) )
+ {
+ $this->software->app->addLink( $mergeWith, $info['pf_group_id'], 'core_pfieldgroups', TRUE );
+ return $mergeWith;
+ }
+
+ if ( !isset( $info['pf_group_name'] ) )
+ {
+ $name = "Converted {$info['pf_group_id']}";
+ $this->software->app->log( 'profile_field_group_missing_name', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ }
+ else
+ {
+ $name = $info['pf_group_name'];
+ unset( $info['pf_group_name'] );
+ }
+
+ if ( !isset( $info['pf_group_order'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(pf_group_order)', 'core_pfields_groups' )->first();
+ $info['pf_group_order'] = $position + 1;
+ }
+
+ $id = $info['pf_group_id'];
+ unset( $info['pf_group_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_pfields_groups', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_pfields_groups' );
+
+ \IPS\Lang::saveCustom( 'core', "core_pfieldgroups_{$inserted_id}", $name );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Profile Field
+ *
+ * @param array $info Data to insert
+ * @param integer|NULL $mergeWith THe ID of an existing profile field to merge this one with, or NULL to create new.
+ * @return boolean|integer The ID of the newly inserted profile field, or FALSE on failure.
+ * @note Profile Field Content for individual members needs to be done during the members step, not here.
+ */
+ public function convertProfileField( $info=array(), $mergeWith=NULL )
+ {
+ /* Get valid fields while taking hooks into account */
+ $validFields = array_merge( static::$fieldTypes, \IPS\core\ProfileFields\Field::$additionalFieldTypes );
+
+ if ( !isset( $info['pf_id'] ) )
+ {
+ $this->software->app->log( 'profile_field_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !is_null( $mergeWith ) )
+ {
+ $this->software->app->addLink( $mergeWith, $info['pf_id'], 'core_pfields_data', TRUE );
+ return $mergeWith;
+ }
+
+ if ( !isset( $info['pf_type'] ) OR !in_array( $info['pf_type'], $validFields ) )
+ {
+ $this->software->app->log( 'profile_field_invalid_type', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['pf_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['pf_name'] ) )
+ {
+ $name = "{$info['pf_type']} Field {$info['pf_id']}";
+ }
+ else
+ {
+ $name = $info['pf_name'];
+ unset( $info['pf_name'] );
+ }
+
+ if ( !isset( $info['pf_desc'] ) )
+ {
+ $desc = '';
+ }
+ else
+ {
+ $desc = $info['pf_desc'];
+ unset( $info['pf_desc'] );
+ }
+
+ if ( isset( $info['pf_content'] ) )
+ {
+ if ( is_array( $info['pf_content'] ) )
+ {
+ $info['pf_content'] = json_encode( $info['pf_content'] );
+ }
+ }
+ else
+ {
+ $info['pf_content'] = NULL;
+ }
+
+ if ( !isset( $info['pf_not_null'] ) )
+ {
+ $info['pf_not_null'] = 0;
+ }
+
+ if ( !isset( $info['pf_member_hide'] ) )
+ {
+ $info['pf_member_hide'] = 1;
+ }
+
+ if ( !isset( $info['pf_max_input'] ) )
+ {
+ $info['pf_max_input'] = 0;
+ }
+
+ if ( !isset( $info['pf_member_edit'] ) )
+ {
+ $info['pf_member_edit'] = 0;
+ }
+
+ if ( !isset( $info['pf_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(pf_position)', 'core_pfields_data' )->first();
+ $info['pf_position'] = $position + 1;
+ }
+
+ if ( !isset( $info['pf_show_on_reg'] ) )
+ {
+ $info['pf_show_on_reg'] = 0;
+ }
+
+ if ( !isset( $info['pf_input_format'] ) )
+ {
+ $info['pf_input_format'] = NULL;
+ }
+
+ if ( !isset( $info['pf_admin_only'] ) )
+ {
+ $info['pf_admin_only'] = 0;
+ }
+
+ if ( !isset( $info['pf_format'] ) )
+ {
+ $info['pf_format'] = NULL;
+ }
+
+ if ( isset( $info['pf_group_id'] ) )
+ {
+ try
+ {
+ $info['pf_group_id'] = $this->software->app->getLink( $info['pf_group_id'], 'core_pfields_groups' );
+ }
+ catch( \OutOfrangeException $e )
+ {
+ /* Create an orphan group */
+ $info['pf_group_id'] = $this->convertProfileFieldGroup( array(
+ 'pf_group_id' => '__orphaned__',
+ 'pf_group_name' => 'Converted',
+ ) );
+ }
+ }
+ else
+ {
+ try
+ {
+ $info['pf_group_id'] = $this->software->app->getLink( '__orphaned__', 'core_pfields_groups' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['pf_group_id'] = $this->convertProfileFieldGroup( array(
+ 'pf_group_id' => '__orphaned__',
+ 'pf_group_name' => 'Converted',
+ ) );
+ }
+ }
+
+ if ( !isset( $info['pf_search_type'] ) )
+ {
+ $info['pf_search_type'] = 'loose';
+ }
+
+ if ( !isset( $info['pf_filtering'] ) )
+ {
+ $info['pf_filtering'] = 0;
+ }
+
+ if ( !isset( $info['pf_multiple'] ) )
+ {
+ $info['pf_multiple'] = 0;
+ }
+
+ $id = $info['pf_id'];
+ unset( $info['pf_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_pfields_data', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_pfields_data' );
+
+ \IPS\Lang::saveCustom( 'core', "core_pfield_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'core', "core_pfield_{$inserted_id}_desc", $desc );
+
+ /* Now... create our column */
+ $columnDefinition = array( 'name' => "field_{$inserted_id}", 'type' => 'TEXT' );
+
+ \IPS\Db::i()->addColumn( 'core_pfields_content', $columnDefinition );
+
+ if ( $info['pf_type'] != 'Upload' )
+ {
+ if ( $columnDefinition['type'] == 'TEXT' )
+ {
+ \IPS\Db::i()->addIndex( 'core_pfields_content', array( 'type' => 'fulltext', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ else
+ {
+ \IPS\Db::i()->addIndex( 'core_pfields_content', array( 'type' => 'key', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Format Member Profile Field Content
+ *
+ * @param int $member_id The ID of the member ID to insert data for.
+ * @param array $fieldInfo The Profile Field Information to format. This SHOULD be in $foreign_id => $content format, however field_$foreign_id => $content is also accepted.
+ * @return array An array of data formatted for core_pfields_content
+ */
+ protected function _formatMemberProfileFieldContent( $member_id, $fieldInfo )
+ {
+ $return = array( 'member_id' => $member_id );
+
+ if ( count( $fieldInfo ) )
+ {
+ foreach( $fieldInfo AS $key => $value )
+ {
+ if ( preg_match( '/^field_(\d+)/i', $key, $matches ) )
+ {
+ $id = str_replace( 'field_', '', $matches[1] );
+ }
+ else
+ {
+ $id = $key;
+ }
+
+ try
+ {
+ $link = $this->software->app->getLink( $id, 'core_pfields_data' );
+
+ /* Make sure the field itself was not removed. */
+ $field = \IPS\Db::i()->select( '*', 'core_pfields_data', array( "pf_id=?", $link ) )->first();
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Link does not exist so we cannot map */
+ continue;
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Field does not exist */
+ continue;
+ }
+
+ /* Too long? */
+ if ( $field['pf_max_input'] )
+ {
+ if ( mb_strlen( $value ) > $field['pf_max_input'] )
+ {
+ $value = mb_substr( $value, 0, $field['pf_max_input'] );
+ }
+ }
+
+ /* Make sure it's not too long for the MySQL TEXT column */
+ if( \strlen( $value ) > 65535 )
+ {
+ $value = \substr( $value, 0, 65535 );
+ $this->software->app->log( "member_{$member_id}_field_truncated", __METHOD__, \IPS\convert\App::LOG_NOTICE, $id );
+ }
+
+ /* If this is a number field, we need to intval() */
+ if ( in_array( $field['pf_type'], array( 'CheckboxSet', 'Member', 'Date', 'Poll', 'YesNo', 'Checkbox', 'Rating', 'Number' ) ) )
+ {
+ if ( in_array( $field['pf_type'], array( 'CheckboxSet', 'Member' ) ) AND $field['pf_multiple'] )
+ {
+ $return[ 'field_' . $link ] = $value;
+ }
+ else
+ {
+ $return[ 'field_' . $link ] = intval( $value );
+ }
+ }
+ else
+ {
+ $return[ 'field_' . $link ] = $value;
+ }
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Convert a Poll
+ *
+ * @param array $info Data to insert
+ * @param array $votes Vote Data
+ * @return boolean|integer The ID of the newly inserted Poll, or false on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertPoll( $info=array(), $votes=array() )
+ {
+ /* Another instance where we really don't need this, but will store if we have it */
+ $hasId = TRUE;
+ if ( !isset( $info['pid'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( !isset( $info['choices'] ) OR !is_array( $info['choices'] ) OR count( $info['choices'] ) == 0 )
+ {
+ $this->software->app->log( 'poll_no_choices', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['pid'] : NULL );
+ return FALSE;
+ }
+
+ /* No longer used */
+ $info['poll_only'] = 0;
+
+ if ( !isset( $info['poll_question'] ) )
+ {
+ $info['poll_question'] = 'Untitled Poll';
+ $this->software->app->log( 'poll_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['pid'] : NULL );
+ }
+
+ if ( isset( $info['start_date'] ) )
+ {
+ if ( $info['start_date'] instanceof \IPS\DateTime )
+ {
+ $info['start_date'] = $info['start_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['start_date'] = time();
+ }
+
+ if ( isset( $info['starter_id'] ) )
+ {
+ try
+ {
+ $info['starter_id'] = $this->software->app->getLink( $info['starter_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['starter_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['starter_id'] = 0;
+ }
+
+ if ( !isset( $info['votes'] ) )
+ {
+ $voteCount = 0;
+ foreach( $votes AS $vote )
+ {
+ if ( isset( $vote['member_choices'] ) AND is_array( $vote['member_choices'] ) )
+ {
+ foreach( $vote['member_choices'] AS $question_id => $choices )
+ {
+ if ( is_array( $vote ) )
+ {
+ $voteCount += count( $vote );
+ }
+ else
+ {
+ $voteCount += 1;
+ }
+ }
+ }
+ }
+ $info['votes'] = $voteCount;
+ }
+
+ if ( !isset( $info['poll_view_voters'] ) )
+ {
+ $info['poll_view_voters'] = 0;
+ }
+
+ /* Encode our choices... there isn't really a whole lot of checking we can do here */
+ $info['choices'] = json_encode( $info['choices'] );
+
+ if ( $hasId )
+ {
+ $id = $info['pid'];
+ unset( $info['pid'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_polls', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_polls' );
+ }
+
+ /* Now do Votes */
+ if ( count( $votes ) )
+ {
+ foreach( $votes AS $member_id => $vote )
+ {
+ $voteHasId = TRUE;
+ if ( !isset( $vote['vid'] ) )
+ {
+ $voteHasId = FALSE;
+ }
+
+ /* Not used */
+ $vote['tid'] = 0;
+ $vote['forum_id'] = 0;
+
+ if ( isset( $vote['vote_date'] ) )
+ {
+ if ( $vote['vote_date'] instanceof \IPS\DateTime )
+ {
+ $vote['vote_date'] = $vote['vote_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $vote['vote_date'] = time();
+ }
+
+ try
+ {
+ $vote['member_id'] = $this->software->app->getLink( $vote['member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Votes need a member account */
+ $this->software->app->log( 'voter_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $id : NULL );
+ continue;
+ }
+
+ if ( !isset( $vote['ip_address'] ) OR filter_var( $info['ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $vote['ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $vote['member_choices'] ) AND is_array( $vote['member_choices'] ) AND count( $vote['member_choices'] ) )
+ {
+ $vote['member_choices'] = json_encode( $vote['member_choices'] );
+ }
+ else
+ {
+ /* We need votes */
+ continue;
+ }
+
+ $vote['poll'] = $inserted_id;
+
+ if ( $voteHasId )
+ {
+ $voteId = $vote['vid'];
+ unset( $vote['vid'] );
+ }
+
+ $voteInsertedId = \IPS\Db::i()->insert( 'core_voters', $vote );
+
+ if ( $voteHasId )
+ {
+ $this->software->app->addLink( $voteInsertedId, $voteId, 'core_voters' );
+ }
+ }
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Profanity Filter
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted profanity filter, or FALSE on failure.
+ */
+ public function convertProfanityFilter( $info=array() )
+ {
+ if ( !isset( $info['wid'] ) )
+ {
+ $this->software->app->log( 'profanity_filter_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['type'] ) )
+ {
+ $this->software->app->log( 'profanity_filter_no_type', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['swop'] ) )
+ {
+ $this->software->app->log( 'profanity_filter_no_swop', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['m_exact'] ) )
+ {
+ $info['m_exact'] = 0;
+ }
+
+ $id = $info['wid'];
+ unset( $info['wid'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_profanity_filters', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_profanity_filters' );
+ return $inserted_id;
+ }
+
+ /**
+ * Convert Question and Answer Spam Prevention
+ *
+ * @param string $info Data to insert
+ * @param array $answers The answers
+ * @return boolean|integer The ID of the newly insert Q&A, or FALSE on failure.
+ */
+ public function convertQuestionAndAnswer( $info, $answers=array() )
+ {
+ $haveId = TRUE;
+ if ( !isset( $info['qa_id'] ) )
+ {
+ $haveId = FALSE;
+ $this->software->app->log( 'convert_question_and_answer_missing_ids', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ }
+
+ if ( !count( $answers ) )
+ {
+ $this->software->app->log( 'convert_question_and_answer_no_answers', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['qa_question'] ) )
+ {
+ $this->software->app->log( 'convert_question_and_answer_no_question', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( $haveId )
+ {
+ $id = $info['qa_id'];
+ unset( $info['qa_id'] );
+ }
+
+ $question = $info['qa_question'];
+ unset( $info['qa_question'] );
+
+ $info['qa_answers'] = json_encode( $answers );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_question_and_answer', $info );
+ \IPS\Lang::saveCustom( 'core', "core_question_and_answer_{$inserted_id}", $question );
+
+ if ( $haveId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_question_and_answer' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Rating
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted rating, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertRating( $info=array() )
+ {
+ $haveId = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $haveId = FALSE;
+ $this->software->app->log( 'rating_missing_ids', __METHOD__, \IPS\convert\App::LOG_NOTICE );
+ }
+
+ if ( !isset( $info['class'] ) )
+ {
+ $this->software->app->log( 'rating_missing_class', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['item_link'] ) )
+ {
+ $this->software->app->log( 'rating_missing_item_link', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['item_id'] ) )
+ {
+ $this->software->app->log( 'rating_missing_item_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rating'] ) )
+ {
+ $this->software->app->log( 'rating_missing_rating', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['ip'] ) OR filter_var( $info['ip'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['ip'] = '127.0.0.1';
+ }
+
+ if ( $haveId )
+ {
+ $id = $info['id'];
+ }
+
+ try
+ {
+ $info['member'] = $this->software->app->getLink( $info['member'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rating_no_member', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['item_id'] = $this->software->app->getLink( $info['item_id'], $info['item_link'] );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rating_no_item', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ unset( $info['item_link'], $info['id'] );
+
+ try
+ {
+ $inserted_id = \IPS\Db::i()->select( 'id', 'core_ratings', array( "class=? AND item_id=? AND member=?", $info['class'], $info['item_id'], $info['member'] ) )->first();
+
+ $this->software->app->log( 'core_rating_duplicate', __METHOD__, \IPS\convert\App::LOG_NOTICE, ( $haveId ) ? $id : NULL );
+ }
+ catch( \UnderflowException $e )
+ {
+ $inserted_id = \IPS\Db::i()->insert( 'core_ratings', $info );
+ }
+
+ if ( $haveId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_ratings' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Report Index
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted report index, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertReportIndex( $info=array() )
+ {
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'report_index_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ $contentClasses = \IPS\Content::routedClasses();
+ if ( !isset( $info['class'] ) OR !in_array( $info['class'], $contentClasses ) )
+ {
+ $this->software->app->log( 'report_index_invalid_class', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ /* The converter will need to pass in the converted content ID */
+ if ( !isset( $info['content_id'] ) )
+ {
+ $this->software->app->log( 'report_index_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ /* Same with this - we need to figure it out in the converter */
+ if ( !isset( $info['perm_id'] ) )
+ {
+ $this->software->app->log( 'report_index_missing_perm', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['status'] ) OR !in_array( $info['status'], array( 1, 2, 3 ) ) )
+ {
+ $info['status'] = 1;
+ }
+
+ if ( !isset( $info['num_reports'] ) )
+ {
+ $info['num_reports'] = 0;
+ }
+
+ if ( !isset( $info['num_comments'] ) )
+ {
+ $info['num_comments'] = 0;
+ }
+
+ if ( isset( $info['first_report_by'] ) )
+ {
+ try
+ {
+ $info['first_report_by'] = $this->software->app->getLink( $info['first_report_by'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['first_report_by'] = 0;
+ }
+ }
+ else
+ {
+ $info['first_report_by'] = 0;
+ }
+
+ if ( isset( $info['first_report_date'] ) )
+ {
+ if ( $info['first_report_date'] instanceof \IPS\DateTime )
+ {
+ $info['first_report_date'] = $info['first_report_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['first_report_date'] = time();
+ }
+
+ if ( isset( $info['last_updated'] ) )
+ {
+ if ( $info['last_updated'] instanceof \IPS\DateTime )
+ {
+ $info['last_updated'] = $info['last_updated']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_updated'] = NULL;
+ }
+
+ if ( isset( $info['author'] ) )
+ {
+ try
+ {
+ $info['author'] = $this->software->app->getLink( $info['author'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['author'] = 0;
+ }
+ }
+ else
+ {
+ $info['author'] = 0;
+ }
+
+ $id = $info['id'];
+ unset( $info['id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_rc_index', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_rc_index' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Report
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted report, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertReport( $info=array() )
+ {
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'report_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['rid'] ) )
+ {
+ try
+ {
+ $info['rid'] = $this->software->app->getLink( $info['rid'], 'core_rc_index' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'report_missing_index', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'report_missing_index', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['report'] ) )
+ {
+ $this->software->app->log( 'report_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['report_by'] ) )
+ {
+ try
+ {
+ $info['report_by'] = $this->software->app->getLink( $info['report_by'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['report_by'] = 0;
+ }
+ }
+ else
+ {
+ $info['report_by'] = 0;
+ }
+
+ if ( isset( $info['date_reported'] ) )
+ {
+ if ( $info['date_reported'] instanceof \IPS\DateTime )
+ {
+ $info['date_reported'] = $info['date_reported']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['date_reported'] = time();
+ }
+
+ if ( !isset( $info['ip_address'] ) OR filter_var( $info['ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['ip_address'] = '127.0.0.1';
+ }
+
+ $id = $info['id'];
+ unset( $info['id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_rc_reports', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_rc_reports' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Report Comment
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted comment, or FALSE on failure.
+ * @note UNLIKE report indexes, this can be done separately.
+ */
+ public function convertReportComment( $info=array() )
+ {
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'report_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['rid'] ) )
+ {
+ try
+ {
+ $info['rid'] = $this->software->app->getLink( $info['rid'], 'core_rc_index' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'report_comment_missing_index', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'report_comment_missing_index', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['comment'] ) )
+ {
+ $this->software->app->log( 'report_comment_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_by'] ) )
+ {
+ try
+ {
+ $info['comment_by'] = $this->software->app->getLink( $info['comment_by'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_by'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_by'] = 0;
+ }
+
+ if ( isset( $info['comment_date'] ) )
+ {
+ if( $info['comment_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_date'] = $info['comment_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_date'] = time();
+ }
+
+ if ( !isset( $info['approved'] ) )
+ {
+ $info['approved'] = 1;
+ }
+
+ if ( isset( $info['edit_date'] ) )
+ {
+ if ( $info['edit_date'] instanceof \IPS\DateTime )
+ {
+ $info['edit_date'] = $info['edit_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['edit_date'] = 0;
+ }
+
+ if ( !isset( $info['ip_address'] ) OR filter_var( $info['ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['ip_address'] = '127.0.0.1';
+ }
+
+ $id = $info['id'];
+ unset( $info['id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_rc_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'core_rc_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert Reputation
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted reputation, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ */
+ public function convertReputation( $info=array() )
+ {
+ /* Another instance where we really don't need this, but will store if we have it */
+ $hasId = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( !isset( $info['app'] ) )
+ {
+ $this->software->app->log( 'reputation_no_app', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['type'] ) )
+ {
+ $this->software->app->log( 'reputation_missing_type', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['type_id'] ) )
+ {
+ $this->software->app->log( 'reputation_missing_type_id', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['member_id'] ) )
+ {
+ $this->software->app->log( 'reputation_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['member_received'] ) )
+ {
+ $this->software->app->log( 'reputation_missing_member_received', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ try
+ {
+ $application = \IPS\Application::load( $info['app'] );
+
+ if ( \IPS\Application::appIsEnabled( $info['app'] ) === FALSE )
+ {
+ throw new \UnexpectedValueException;
+ }
+ }
+ catch( \UnexpectedValueException $e )
+ {
+ $this->software->app->log( 'reputation_app_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['rep_date'] ) )
+ {
+ if ( $info['rep_date'] instanceof \IPS\DateTime )
+ {
+ $info['rep_date'] = $info['rep_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['rep_date'] = time();
+ }
+
+ if ( !isset( $info['rep_rating'] ) OR $info['rep_rating'] > 1 )
+ {
+ $info['rep_rating'] = 1;
+ }
+ else
+ {
+ if ( $info['rep_rating'] < -1 )
+ {
+ $info['rep_rating'] = -1;
+ }
+ }
+
+ /* Get our content class */
+ $contentClass = NULL;
+ foreach( $application->extensions( 'core', 'ContentRouter' ) AS $extension )
+ {
+ foreach( $extension->classes AS $contentClass )
+ {
+ if ( isset( $contentClass::$reputationType ) )
+ {
+ if ( $contentClass::$reputationType == $info['type'] )
+ {
+ break 2;
+ }
+ }
+
+ if ( isset( $contentClass::$commentClass ) )
+ {
+ $commentClass = $contentClass::$commentClass;
+
+ if ( isset( $commentClass::$reputationType ) )
+ {
+ if ( $commentClass::$reputationType == $info['type'] )
+ {
+ $contentClass = $commentClass;
+ break 2;
+ }
+ }
+ }
+
+ if ( isset( $contentClass::$reviewClass ) )
+ {
+ $reviewClass = $contentClass::$reviewClass;
+
+ if ( isset( $reviewClass::$reputationType ) )
+ {
+ if ( $reviewClass::$reputationType == $info['type'] )
+ {
+ $contentClass = $reviewClass;
+ break 2;
+ }
+ }
+ }
+ }
+ }
+
+ if ( is_null( $contentClass ) )
+ {
+ $this->software->app->log( 'reputation_type_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ $info['rep_class'] = $contentClass;
+
+ try
+ {
+ $info['member_id'] = $this->software->app->getLink( $info['member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'reputation_member_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['type_id'] = $this->software->app->getLink( $info['type_id'], $contentClass::$databaseTable );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'reputation_type_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ $info['class_type_id_hash'] = md5( $contentClass . ':' . $info['type_id'] );
+
+ try
+ {
+ $info['member_received'] = $this->software->app->getLink( $info['member_received'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'reputation_member_received_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['id'];
+ unset( $info['id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_reputation_index', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_reputation_index' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert Reputation Level
+ *
+ * @param array $info Data to insert
+ * @param string|NULL $badgepath Path to Reputation Badge file.
+ * @param string|NULL $badgedata Binary data for Reputation Badge file.
+ * @return boolean|integer The ID of the newly inserted reputation level, or FALSE on failure.
+ */
+ public function convertReputationLevel( $info=array(), $badgepath=NULL, $badgedata=NULL )
+ {
+ /* Another instance where we really don't need this, but will store if we have it */
+ $hasId = TRUE;
+ if ( !isset( $info['level_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( !isset( $info['level_points'] ) )
+ {
+ $info['level_points'] = 0;
+ }
+
+ if ( !isset( $info['level_title'] ) )
+ {
+ $name = "Reputation {$info['level_points']}";
+ $this->software->app->log( 'reputation_level_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ }
+ else
+ {
+ $name = $info['level_title'];
+ unset( $info['level_title'] );
+ }
+
+ if ( isset( $info['level_image'] ) AND ( !is_null( $badgepath ) OR !is_null( $badgedata ) ) )
+ {
+ try
+ {
+ if ( is_null( $badgedata ) AND !is_null( $badgepath ) )
+ {
+ $badgedata = file_get_contents( $badgepath );
+ }
+ $file = \IPS\File::create( 'core_Theme', $info['level_image'], $badgedata );
+ $info['level_image'] = (string) $file;
+ }
+ catch( \Exception $e )
+ {
+ $info['level_image'] = '';
+ }
+ catch( \ErrorException $e )
+ {
+ $info['level_image'] = '';
+ }
+ }
+ else
+ {
+ $info['level_image'] = '';
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['level_id'];
+ unset( $info['level_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_reputation_levels', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_reputation_levels' );
+ }
+
+ \IPS\Lang::saveCustom( 'core', "core_reputation_level_{$inserted_id}", $name );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Setting
+ *
+ * @param array $settings Settings to convert
+ * @return boolean|array An array of settings changed, or FALSE on failure.
+ */
+ public function convertSettings( $settings=array() )
+ {
+ if ( !count( $settings ) )
+ {
+ $this->software->app->log( 'no_settings_to_convert', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ foreach( $settings AS $setting )
+ {
+ \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $setting['value'] ), array( "conf_key=?", $setting['key'] ) );
+ }
+
+ unset( \IPS\Data\Store::i()->settings );
+
+ return $settings;
+ }
+
+ /**
+ * Convert a Tag
+ *
+ * @param array $info Data to insert
+ * @return boolean|integer The ID of the newly inserted tag, or FALSE on failure.
+ * @note Like Follows, this should be done when the actual content it's attached too is being converted.
+ * @note core_tags_cache and core_tags_perms need to be populated by the converter.
+ */
+ public function convertTag( $info=array() )
+ {
+ /* Another instance where we really don't need this, but will store if we have it */
+ $hasId = TRUE;
+ if ( !isset( $info['tag_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ /* Basic checks to make sure we have what we need. */
+ foreach( array( 'tag_meta_app', 'tag_meta_area', 'tag_meta_id', 'tag_meta_parent_id', 'tag_text' ) AS $column )
+ {
+ if ( !isset( $info[$column] ) OR empty( $info[$column] ) )
+ {
+ $this->software->app->log( "tag_missing_{$column}", __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['tag_id'] : NULL );
+ return FALSE; # return here - all of these are required so don't bother proceeding
+ }
+ }
+
+ /* let's do some set up */
+ try
+ {
+ $application = \IPS\Application::load( $info['tag_meta_app'] );
+
+ if ( \IPS\Application::appIsEnabled( $info['tag_meta_app'] ) === FALSE )
+ {
+ throw new \UnexpectedValueException;
+ }
+ }
+ catch( \UnexpectedValueException $e )
+ {
+ $this->software->app->log( 'tag_app_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['tag_id'] : NULL );
+ return FALSE;
+ }
+
+ try
+ {
+ $info['tag_member_id'] = $this->software->app->getLink( $info['tag_member_id'], 'core_members' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Tags can be added by guests */
+ $info['tag_member_id'] = 0;
+ }
+
+ if ( isset( $info['tag_added'] ) )
+ {
+ if ( $info['tag_added'] instanceof \IPS\DateTime )
+ {
+ $info['tag_added'] = $info['tag_added']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['tag_added'] = time();
+ }
+
+ if ( !isset( $info['tag_prefix'] ) )
+ {
+ $info['tag_prefix'] = 0;
+ }
+
+ $isVisible = 1;
+ if ( isset( $info['tag_visible'] ) )
+ {
+ $isVisible = $info['tag_visible'];
+ unset( $info['tag_visible'] );
+ }
+
+ /* Figure out our content class */
+ $contentClass = NULL;
+ foreach( $application->extensions( 'core', 'ContentRouter' ) AS $extension )
+ {
+ foreach( $extension->classes AS $contentClass )
+ {
+ if ( $contentClass::$module == $info['tag_meta_area'] )
+ {
+ break 2;
+ }
+ }
+ }
+
+ if ( is_null( $contentClass ) )
+ {
+ $this->software->app->log( 'tag_area_invalid', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['tag_id'] : NULL );
+ return FALSE;
+ }
+
+ /* Tag text should be lower case and not start or end with a space */
+ $info['tag_text'] = \mb_strtolower( trim( $info['tag_text'] ) );
+
+ $nodeClass = $contentClass::$containerNodeClass;
+
+ try
+ {
+ $table = $contentClass::$databaseTable;
+ if ( isset( $info['tag_meta_link'] ) )
+ {
+ $table = $info['tag_meta_link'];
+ unset( $info['tag_meta_link'] );
+ }
+
+ $info['tag_meta_id'] = $this->software->app->getLink( $info['tag_meta_id'], $table );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'tag_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['tag_id'] : NULL );
+ return FALSE;
+ }
+
+ try
+ {
+ $parentTable = $nodeClass::$databaseTable;
+
+ if ( isset( $info['tag_meta_parent_link'] ) )
+ {
+ $parentTable = $info['tag_meta_parent_link'];
+ unset( $info['tag_meta_parent_link'] );
+ }
+ $info['tag_meta_parent_id'] = $this->software->app->getLink( $info['tag_meta_parent_id'], $parentTable );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'tag_parent_orphaned', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['tag_id'] : NULL );
+ return FALSE;
+ }
+
+ /* Set up our lookup md5's */
+ $info['tag_aai_lookup'] = md5( $info['tag_meta_app'] . ';' . $info['tag_meta_area'] . ';' . $info['tag_meta_id'] );
+ $info['tag_aap_lookup'] = md5( $nodeClass::$permApp . ';' . $nodeClass::$permType . ';' . $info['tag_meta_parent_id'] );
+
+ if ( $hasId )
+ {
+ $id = $info['tag_id'];
+ unset( $info['tag_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'core_tags', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'core_tags' );
+ }
+
+ return $inserted_id;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Downloads.php b/applications/convert/sources/Library/Downloads.php
new file mode 100644
index 00000000..859d20dc
--- /dev/null
+++ b/applications/convert/sources/Library/Downloads.php
@@ -0,0 +1,1361 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Downloads extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'downloads';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertDownloadsCfields':
+ $return[ $k ] = array(
+ 'step_method' => 'convertDownloadsCfields',
+ 'step_title' => 'convert_downloads_cfields',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'downloads_cfields' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'downloads_cfields',
+ );
+ break;
+
+ case 'convertDownloadsCategories':
+ $dependencies = array();
+
+ if ( array_key_exists( 'convertDownloadsCfields', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertDownloadsCfields';
+ }
+
+ $return[ $k ] = array(
+ 'step_method' => 'convertDownloadsCategories',
+ 'step_title' => 'convert_downloads_categories',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'downloads_categories' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'downloads_categories',
+ );
+ break;
+
+ case 'convertDownloadsFiles':
+ $dependencies = array();
+ if ( array_key_exists( 'convertDownloadsCategories', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertDownloadsCategories';
+ }
+
+ if ( array_key_exists( 'convertDownloadsCfields', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertDownloadsCfields';
+ }
+
+ $return[ $k ] = array(
+ 'step_method' => 'convertDownloadsFiles',
+ 'step_title' => 'convert_downloads_files',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'downloads_files' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 250,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'downloads_files',
+ );
+ break;
+
+ case 'convertDownloadsComments':
+ $return[ $k ] = array(
+ 'step_method' => 'convertDownloadsComments',
+ 'step_title' => 'convert_downloads_comments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'downloads_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertDownloadsFiles' ),
+ 'link_type' => 'downloads_comments',
+ );
+ break;
+
+ case 'convertDownloadsReviews':
+ $return[ $k ] = array(
+ 'step_method' => 'convertDownloadsReviews',
+ 'step_title' => 'convert_downloads_reviews',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'downloads_reviews' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertDownloadsFiles' ),
+ 'link_type' => 'downloads_reviews',
+ );
+ break;
+
+ case 'convertAttachments':
+ $return[ $k ] = array(
+ 'step_method' => 'convertAttachments',
+ 'step_title' => 'convert_attachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( "location_key=?", 'downloads_Downloads' ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertDownloadsFiles', 'convertDownloadsComments', 'convertDownloadsReviews' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertDownloadsCategories':
+ $return['convertDownloadsCategories'] = array( 'downloads_categories' => NULL );
+ break;
+
+ case 'convertDownloadsCfields':
+ if ( $method == $k )
+ {
+ foreach( \IPS\Db::i()->select( '*', 'downloads_cfields' ) AS $field )
+ {
+ \IPS\Db::i()->dropColumn( 'downloads_ccontent', "field_{$field['cf_id']}" );
+ }
+ }
+ $return['convertDownloadsCfields'] = array( 'downloads_cfields' => NULL );
+ break;
+
+ case 'convertDownloadsFiles':
+ $return['convertDownloadsFiles'] = array( 'downloads_files' => NULL, 'downloads_files_records' => NULL );
+ break;
+
+ case 'convertDownloadsComments':
+ $return['convertDownloadsComments'] = array( 'downloads_comments' => NULL );
+ break;
+
+ case 'convertDownloadsReviews':
+ $return['convertDownloadsReviews'] = array( 'downloads_reviews' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $return['convertAttachments'] = array( 'core_attachments' => \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( "location_key=?", 'downloads_Downloads' ) ), 'core_attachments_map' => array( "location_key=?", 'downloads_Downloads' ) );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a Custom Field
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted field, or FALSE on failure.
+ */
+ public function convertDownloadsCfield( $info=array() )
+ {
+ $validFields = array_merge( static::$fieldTypes, \IPS\downloads\Field::$additionalFieldTypes );
+
+ if ( !isset( $info['cf_id'] ) )
+ {
+ $this->software->app->log( 'downloads_cfield_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['cf_type'] ) OR !in_array( $info['cf_type'], $validFields ) )
+ {
+ $this->software->app->log( 'downloads_cfield_invalid_type', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['cf_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['cf_content'] ) )
+ {
+ if ( is_array( $info['cf_content'] ) )
+ {
+ $info['cf_content'] = json_encode( $info['cf_content'] );
+ }
+ }
+ else
+ {
+ $info['cf_content'] = json_encode( array() );
+ }
+
+ if ( !isset( $info['cf_not_null'] ) )
+ {
+ $info['cf_not_null'] = 1;
+ }
+
+ if ( !isset( $info['cf_max_input'] ) )
+ {
+ $info['cf_max_input'] = 0;
+ }
+
+ if ( !isset( $info['cf_input_format'] ) )
+ {
+ $info['cf_input_format'] = '';
+ }
+
+ if ( !isset( $info['cf_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(cf_position)', 'downloads_cfields' )->first();
+
+ $info['cf_position'] = $position + 1;
+ }
+
+ if ( !isset( $info['cf_topic'] ) )
+ {
+ $info['cf_topic'] = 0;
+ }
+
+ if ( !isset( $info['cf_search_type'] ) )
+ {
+ $info['cf_search_type'] = 'loose';
+ }
+
+ if ( !isset( $info['cf_multiple'] ) )
+ {
+ $info['cf_multiple'] = 0;
+ }
+
+ if ( !isset( $info['cf_format'] ) )
+ {
+ $info['cf_format'] = '';
+ }
+
+ $id = $info['cf_id'];
+ $fieldName = isset( $info['cf_name'] ) ? $info['cf_name'] : NULL;
+ unset( $info['cf_id'], $info['cf_name'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'downloads_cfields', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'downloads_cfields' );
+
+ /* Field Name */
+ \IPS\Lang::saveCustom( 'downloads', "downloads_field_{$inserted_id}", $fieldName ?: "Converted Field {$inserted_id}" );
+
+ /* Now... create our column */
+ $columnDefinition = array( 'name' => "field_{$inserted_id}" );
+
+ switch( $info['cf_type'] )
+ {
+ case 'CheckboxSet':
+ case 'Member':
+ if ( $info['cf_multiple'] )
+ {
+ $columnDefinition['type'] = 'TEXT';
+ }
+ else
+ {
+ $columnDefinition['type'] = 'INT';
+ $columnDefinition['length'] = 10;
+ }
+ break;
+
+ case 'Date':
+ case 'Poll':
+ $columnDefinition['type'] = 'INT';
+ $columnDefinition['length'] = 10;
+ break;
+
+ case 'Editor':
+ case 'TextArea':
+ case 'Upload':
+ case 'Address':
+ case 'Codemirror':
+ case 'Select':
+ $columnDefinition['type'] = 'TEXT';
+ break;
+
+ case 'Email':
+ case 'Password':
+ case 'Tel':
+ case 'Text':
+ case 'Url':
+ case 'Color':
+ case 'Radio':
+ case 'Number':
+ $columnDefinition['type'] = 'VARCHAR';
+ $columnDefinition['length'] = 255;
+ break;
+
+ case 'YesNo':
+ case 'Checkbox':
+ case 'Rating':
+ $columnDefinition['type'] = 'TINYINT';
+ $columnDefinition['length'] = 1;
+ break;
+ }
+
+ if ( isset( $info['cf_max_input'] ) AND $info['cf_max_input'] )
+ {
+ $columnDefinition['length'] = $info['cf_max_input'];
+ }
+
+ \IPS\Db::i()->addColumn( 'downloads_ccontent', $columnDefinition );
+
+ if ( $info['cf_type'] != 'Upload' )
+ {
+ if ( $columnDefinition['type'] == 'TEXT' )
+ {
+ \IPS\Db::i()->addIndex( 'downloads_ccontent', array( 'type' => 'fulltext', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ else
+ {
+ \IPS\Db::i()->addIndex( 'downloads_ccontent', array( 'type' => 'key', 'name' => $columnDefinition['name'], 'columns' => array( $columnDefinition['name'] ) ) );
+ }
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Format Downloads Custom Field Content
+ *
+ * @param int $file_id The ID of the file to insert data for.
+ * @param array $fieldInfo The Custom Field Information to format. This SHOULD be in $foreign_id => $content format, however field_$foreign_id => $content is also accepted.
+ * @return array An array of data formatted for downloads_ccontent
+ */
+ protected function _formatCustomFieldContent( $file_id, $fieldInfo )
+ {
+ $return = array( 'file_id' => $file_id );
+
+ if ( count( $fieldInfo ) )
+ {
+ foreach( $fieldInfo as $key => $value )
+ {
+ if ( preg_match( '/^field_(\d+)/i', $key, $matches ) )
+ {
+ $id = str_replace( 'field_', '', $matches[1] );
+ }
+ else
+ {
+ $id = $key;
+ }
+
+ try
+ {
+ $link = $this->software->app->getLink( $id, 'downloads_cfields' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Does not exist - skip */
+ continue;
+ }
+
+ $return[ 'field_' . $link ] = $value;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Convert a category
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted category, or FALSE on failure.
+ */
+ public function convertDownloadsCategory( $info=array() )
+ {
+ if ( !isset( $info['cid'] ) )
+ {
+ $this->software->app->log( 'downloads_category_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['cparent'] ) )
+ {
+ try
+ {
+ $info['cparent'] = $this->software->app->getLink( $info['cparent'], 'downloads_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['cconv_parent'] = $info['cparent'];
+ }
+ }
+ else
+ {
+ $info['cparent'] = 0;
+ }
+
+ if ( !isset( $info['cname'] ) )
+ {
+ $name = "Untitled Category {$info['cid']}";
+ }
+ else
+ {
+ $name = $info['cname'];
+ unset( $info['cname'] );
+ }
+
+ if ( !isset( $info['cdesc'] ) )
+ {
+ $desc = '';
+ }
+ else
+ {
+ $desc = $info['cdesc'];
+ unset( $info['cdesc'] );
+ }
+
+ if ( !isset( $info['copen'] ) )
+ {
+ $info['copen'] = 1;
+ }
+
+
+ if ( !isset( $info['cposition'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(cposition)', 'downloads_categories' )->first();
+
+ $info['cposition'] = $position + 1;
+ }
+
+ if ( isset( $info['ccfields'] ) )
+ {
+ if ( !is_array( $info['ccfields'] ) )
+ {
+ $info['ccfields'] = explode( ',', $info['ccfields'] );
+ }
+
+ $newCfields = array();
+ if ( count( $info['ccfields'] ) )
+ {
+ foreach( $info['ccfields'] AS $field )
+ {
+ try
+ {
+ $newCfields[] = $this->software->app->getLink( $field, 'downloads_cfields' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+ }
+
+ if ( count( $newCfields ) )
+ {
+ $info['ccfields'] = implode( ',', $newCfields );
+ }
+ else
+ {
+ $info['ccfields'] = NULL;
+ }
+ }
+ else
+ {
+ $info['ccfields'] = NULL;
+ }
+
+ $info['cname_furl'] = \IPS\Http\Url::seoTitle( $name );
+
+ if ( !isset( $info['ctags_disabled'] ) )
+ {
+ $info['ctags_disabled'] = 0;
+ }
+
+ if ( !isset( $info['ctags_noprefixes'] ) )
+ {
+ $info['ctags_noprefixes'] = 0;
+ }
+
+ /* Not Used */
+ $info['ctags_predefined'] = NULL;
+
+ $bitoptions = 0;
+ foreach( \IPS\downloads\Category::$bitOptions['bitoptions']['bitoptions'] AS $key => $value )
+ {
+ if ( isset( $info['cbitoptions'][$key] ) AND $info['cbitoptions'][$key] )
+ {
+ $bitoptions += $value;
+ }
+ }
+ $info['cbitoptions'] = $bitoptions;
+
+ if ( isset( $info['ctypes'] ) )
+ {
+ if ( is_array( $info['ctypes'] ) )
+ {
+ $info['ctypes'] = implode( ',', $info['ctypes'] );
+ }
+ }
+ else
+ {
+ $info['ctypes'] = NULL;
+ }
+
+ if ( !isset( $info['csortorder'] ) OR !in_array( $info['csortoder'], array( 'updated', 'last_comment', 'title', 'rating', 'date', 'num_comments', 'num_reviews', 'views' ) ) )
+ {
+ $info['csortorder'] = 'updated';
+ }
+
+ if ( !isset( $info['cmaxfile'] ) )
+ {
+ $info['cmaxfile'] = NULL;
+ }
+
+ if ( !isset( $info['cmaxss'] ) )
+ {
+ $info['cmaxss'] = 0;
+ }
+
+ if ( isset( $info['cmaxdims'] ) )
+ {
+ if ( is_array( $info['cmaxdims'] ) )
+ {
+ $info['cmaxdims'] = implode( 'x', $info['cmaxdims'] );
+ }
+ }
+ else
+ {
+ $info['cmaxdims'] = '0x0';
+ }
+
+ if ( !isset( $info['cversioning'] ) )
+ {
+ $info['cversioning'] = NULL;
+ }
+
+ if ( !isset( $info['clog'] ) )
+ {
+ $info['clog'] = 1;
+ }
+
+ if ( isset( $info['cforum_id'] ) )
+ {
+ try
+ {
+ $info['cforum_id'] = $this->software->app->getSiblingLink( $info['cforum_id'], 'forums_forums', 'forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['cforum_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['cforum_id'] = 0;
+ }
+
+ if ( !isset( $info['ctopic_prefix'] ) )
+ {
+ $info['ctopic_prefix'] = '';
+ }
+
+ if ( !isset( $info['ctopic_suffix'] ) )
+ {
+ $info['ctopic_suffix'] = '';
+ }
+
+ /* Can't know this */
+ $info['clast_file_id'] = 0;
+ $info['clast_file_date'] = 0;
+
+ $id = $info['cid'];
+ unset( $info['cid'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'downloads_categories', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'downloads_categories' );
+
+
+ \IPS\Lang::saveCustom( 'downloads', "downloads_category_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'downloads', "downloads_category_{$inserted_id}_desc", $desc );
+
+ \IPS\Db::i()->update( 'downloads_categories', array( "cparent" => $inserted_id ), array( "cconv_parent=? AND cconv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a file.
+ *
+ * @param array $info Data to insert
+ * @param array $records Record Data to insert
+ * @param array $customFields Custom Field Data to insert
+ * @return integer|boolean The ID of the newly inserted file, or FALSE on failure.
+ */
+ public function convertDownloadsFile( $info=array(), $records=array(), $customFields=array() )
+ {
+ if ( !isset( $info['file_id'] ) )
+ {
+ $this->software->app->log( 'downloads_file_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !count( $records ) )
+ {
+ $this->software->app->log( 'downloads_file_no_records', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['file_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['file_desc'] ) )
+ {
+ $this->software->app->log( 'downloads_file_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['file_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['file_name'] ) )
+ {
+ $info['file_name'] = "Untitled File {$info['file_id']}";
+ $this->software->app->log( 'downloads_file_no_title', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['file_id'] );
+ }
+
+ if ( isset( $info['file_cat'] ) )
+ {
+ try
+ {
+ $info['file_cat'] = $this->software->app->getLink( $info['file_cat'], 'downloads_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['file_cat'] = $this->_orphanedFilesCategory();
+ }
+ }
+ else
+ {
+ $info['file_cat'] = $this->_orphanedFilesCategory();
+ }
+
+ if ( !isset( $info['file_open'] ) )
+ {
+ $info['file_open'] = 1;
+ }
+
+ /* No longer used? */
+ $info['file_broken'] = 0;
+ $info['file_broken_reason'] = NULL;
+ $info['file_broken_info'] = NULL;
+ $info['file_votes'] = NULL;
+ $info['file_new'] = 0;
+ $info['file_topicseoname'] = NULL;
+ $info['file_post_key'] = NULL;
+
+ if ( !isset( $info['file_rating'] ) )
+ {
+ $info['file_rating'] = 0;
+ }
+
+ /* Let's get the counts out of the way */
+ foreach( array( 'file_views', 'file_downloads', 'file_pendcomments', 'file_comments', 'file_reviews', 'file_unapproved_comments', 'file_hidden_comments', 'file_unapproved_reviews', 'file_hidden_reviews' ) AS $count )
+ {
+ if ( !isset( $info[$count] ) )
+ {
+ $info[$count] = 0;
+ }
+ }
+
+ if ( isset( $info['file_submitted'] ) )
+ {
+ if ( $info['file_submitted'] instanceof \IPS\DateTime )
+ {
+ $info['file_submitted'] = $info['file_submitted']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['file_submitted'] = time();
+ }
+
+ if ( isset( $info['file_updated'] ) )
+ {
+ if ( $info['file_updated'] instanceof \IPS\DateTime )
+ {
+ $info['file_updated'] = $info['file_updated']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['file_updated'] = $info['file_submitted'];
+ }
+
+ /* We recalculate this later anyway */
+ $info['file_size'] = 0;
+
+ if ( isset( $info['file_submitter'] ) )
+ {
+ try
+ {
+ $info['file_submitter'] = $this->software->app->getLink( $info['file_submitter'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['file_submitter'] = 0;
+ }
+ }
+ else
+ {
+ $info['file_submitter'] = 0;
+ }
+
+ if ( isset( $info['file_approver'] ) )
+ {
+ try
+ {
+ $info['file_approver'] = $this->software->app->getLink( $info['file_approver'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['file_approver'] = 0;
+ }
+ }
+ else
+ {
+ $info['file_approver'] = 0;
+ }
+
+ if ( isset( $info['file_approvedon'] ) )
+ {
+ if ( $info['file_approvedon'] instanceof \IPS\DateTime )
+ {
+ $info['file_approvedon'] = $info['file_approvedon']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['file_approvedon'] = 0;
+ }
+
+ if ( isset( $info['file_topicid'] ) )
+ {
+ try
+ {
+ $info['file_topicid'] = $this->software->app->getSiblingLink( $info['file_topicid'], 'forums_topics', 'forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['file_topicid'] = 0;
+ }
+ }
+ else
+ {
+ $info['file_topicid'] = 0;
+ }
+
+ if ( !isset( $info['file_ipaddress'] ) OR filter_var( $info['file_ipaddress'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['file_ipaddress'] = '127.0.0.1';
+ }
+
+ $info['file_name_furl'] = \IPS\Http\url::seoTitle( $info['file_name'] );
+
+ if ( \IPS\Application::appIsEnabled( 'nexus' ) )
+ {
+ // @todo I need the Commerce Libraries for this
+ }
+ else
+ {
+ $info['file_cost'] = NULL;
+ $info['file_nexus'] = NULL;
+ $info['file_renewal_term'] = 0;
+ $info['file_renewal_units'] = NULL;
+ $info['file_renewal_price'] = NULL;
+ }
+
+ if ( !isset( $info['file_version'] ) )
+ {
+ $info['file_version'] = NULL;
+ }
+
+ if ( !isset( $info['file_changelog'] ) )
+ {
+ $info['file_changelog'] = NULL;
+ }
+
+ if ( !isset( $info['file_featured'] ) )
+ {
+ $info['file_featured'] = 0;
+ }
+
+ if ( !isset( $info['file_pinned'] ) )
+ {
+ $info['file_pinned'] = 0;
+ }
+
+ /* We'll set this later */
+ $info['file_primary_screenshot'] = 0;
+
+ if ( !isset( $info['file_locked'] ) )
+ {
+ $info['file_locked'] = 0;
+ }
+
+ if ( isset( $info['file_last_comment'] ) )
+ {
+ if ( $info['file_last_comment'] instanceof \IPS\DateTime )
+ {
+ $info['file_last_comment'] = $info['file_last_comment']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['file_last_comment'] = $info['file_submitted'];
+ }
+
+ if ( isset( $info['file_last_review'] ) )
+ {
+ if ( $info['file_last_review'] instanceof \IPS\DateTime )
+ {
+ $info['file_last_review'] = $info['file_last_review']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['file_last_review'] = $info['file_submitted'];
+ }
+
+ $id = $info['file_id'];
+ unset( $info['file_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'downloads_files', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'downloads_files' );
+
+ /* Now our records */
+ $totalFileSize = 0;
+ $primaryScreenshot = FALSE;
+ foreach( $records AS $record )
+ {
+ /* We don't really need this */
+ $hasId = TRUE;
+ if ( !isset( $record['record_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ $record['record_post_key'] = NULL;
+ $record['record_file_id'] = $inserted_id;
+ $record['record_link_type'] = NULL;
+ $record['record_no_watermark'] = NULL;
+
+ if ( !isset( $record['record_type'] ) OR !in_array( $record['record_type'], array( 'upload', 'ssupload', 'link' ) ) )
+ {
+ $record['record_type'] = 'upload';
+ }
+
+ if ( !isset( $record['record_backup'] ) )
+ {
+ $record['record_backup'] = 0;
+ }
+
+ if ( !isset( $record['record_default'] ) OR $record['record_type'] == 'upload' OR $record['record_type'] == 'link' OR $primaryScreenshot !== FALSE )
+ {
+ $record['record_default'] = 0;
+ }
+
+ if ( !isset( $record['record_realname'] ) )
+ {
+ if ( isset( $record['file_path'] ) AND !is_null( $record['file_path'] ) )
+ {
+ $fileName = explode( '/', $record['file_path'] );
+ $fileName = array_pop( $fileName );
+ $record['record_realname'] = $fileName;
+ }
+ }
+
+ try
+ {
+ if ( !isset( $record['file_data'] ) OR is_null( $record['file_data'] ) )
+ {
+ $record['file_data'] = file_get_contents( $record['file_path'] );
+ }
+
+ if ( $record['record_type'] != 'link' )
+ {
+ unset( $record['file_path'] );
+ }
+
+ if ( $record['record_type'] == 'link' )
+ {
+ $record['record_location'] = $record['file_path'];
+ unset( $record['file_path'] );
+
+ $record['record_size'] = 0;
+ }
+ else if ( $record['record_type'] == 'upload' )
+ {
+ $file = \IPS\File::create( 'downloads_Files', $record['record_realname'], $record['file_data'] );
+ $record['record_location'] = (string) $file;
+ $record['record_size'] = $file->filesize();
+
+ if ( !$record['record_backup'] )
+ {
+ $totalFileSize += $record['record_size'];
+ }
+ }
+ else
+ {
+ $file = \IPS\File::create( 'downloads_Screenshots', $record['record_realname'], $record['file_data'] );
+ $record['record_location'] = (string) $file;
+ $record['record_thumb'] = (string) $file->thumbnail( 'downloads_Files' );
+ $record['record_size'] = $file->filesize();
+ }
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $record['record_id'] : NULL );
+ continue;
+ }
+ catch( \ErrorException $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $record['record_id'] : NULL );
+ continue;
+ }
+
+ unset( $record['file_data'] );
+
+ if ( isset( $record['record_time'] ) )
+ {
+ if ( $record['record_time'] instanceof \IPS\DateTime )
+ {
+ $record['record_time'] = $record['record_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $record['record_time'] = $info['file_submitted'];
+ }
+
+ if ( $hasId )
+ {
+ $recordId = $record['record_id'];
+ unset( $record['record_id'] );
+ }
+
+ $recordInsertedId = \IPS\Db::i()->insert( 'downloads_files_records', $record );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $recordInsertedId, $recordId, 'downloads_files_records' );
+ }
+
+ if ( $record['record_type'] == 'ssupload' AND $record['record_default'] > 0 AND $primaryScreenshot === FALSE )
+ {
+ $primaryScreenshot = $recordInsertedId;
+ }
+ }
+
+ \IPS\Db::i()->update( 'downloads_files', array( 'file_primary_screenshot' => $primaryScreenshot ), array( "file_id=?", $inserted_id ) );
+ \IPS\Db::i()->replace( 'downloads_ccontent', $this->_formatCustomFieldContent( $inserted_id, $customFields ), TRUE );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Get Orphaned Files Category
+ *
+ * @return integer The Category ID.
+ */
+ protected function _orphanedFilesCategory()
+ {
+ try
+ {
+ return $this->software->app->getLink( '__orphaned__', 'downloads_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ return $this->convertDownloadsCategory( array(
+ 'cid' => '__orphaned__',
+ 'cname' => "Converted Files",
+ ) );
+ }
+ }
+
+ /**
+ * Convert a comment
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted comment, or FALSE on failure.
+ */
+ public function convertDownloadsComment( $info=array() )
+ {
+ if ( !isset( $info['comment_id'] ) )
+ {
+ $this->software->app->log( 'downloads_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_fid'] ) )
+ {
+ try
+ {
+ $info['comment_fid'] = $this->software->app->getLink( $info['comment_fid'], 'downloads_files' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'downloads_comment_missing_file', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'downloads_comment_missing_file', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['comment_text'] ) )
+ {
+ $this->software->app->log( 'downloads_comment_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_mid'] ) )
+ {
+ try
+ {
+ $info['comment_mid'] = $this->software->app->getLink( $info['comment_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_mid'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_mid'] = 0;
+ }
+
+ if ( isset( $info['comment_date'] ) )
+ {
+ if ( $info['comment_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_date'] = $info['comment_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_date'] = time();
+ }
+
+ if ( !isset( $info['comment_open'] ) )
+ {
+ $info['comment_open'] = 1;
+ }
+
+ if ( !isset( $info['comment_append_edit'] ) )
+ {
+ $info['comment_append_edit'] = 0;
+ }
+
+ if ( isset( $info['comment_edit_time'] ) )
+ {
+ if ( $info['comment_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['comment_edit_time'] = $info['comment_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_edit_time'] = 0;
+ }
+
+ if ( !isset( $info['comment_ip_address'] ) OR filter_var( $info['comment_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['comment_ip_address'] = '127.0.0.1';
+ }
+
+ if ( !isset( $info['comment_author'] ) )
+ {
+ $author = \IPS\Member::load( $info['comment_mid'] );
+
+ if ( $author->member_id )
+ {
+ $info['comment_author'] = $author->name;
+ }
+ else
+ {
+ $info['comment_author'] = "Guest";
+ }
+ }
+
+ $id = $info['comment_id'];
+ unset( $info['comment_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'downloads_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'downloads_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a review
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted review, or FALSE on failure.
+ */
+ public function convertDownloadsReview( $info=array() )
+ {
+ if ( !isset( $info['review_id'] ) )
+ {
+ $this->software->app->log( 'download_review_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( empty( $info['review_text'] ) )
+ {
+ $this->software->app->log( 'download_review_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['review_rating'] ) OR $info['review_rating'] < 1 )
+ {
+ $this->software->app->log( 'download_review_missing_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_mid'] ) )
+ {
+ try
+ {
+ $info['review_mid'] = $this->software->app->getLink( $info['review_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'download_review_missing_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'download_review_missing_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_fid'] ) )
+ {
+ try
+ {
+ $info['review_fid'] = $this->software->app->getLink( $info['review_fid'], 'downloads_files' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'download_review_missing_image', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+
+ if ( !isset( $info['review_author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['review_mid'] );
+ $info['review_author_name'] = $author->name;
+ }
+
+ if ( isset( $info['review_date'] ) )
+ {
+ if ( $info['review_date'] instanceof \IPS\DateTime )
+ {
+ $info['review_date'] = $info['review_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_date'] = time();
+ }
+
+ if ( !isset( $info['review_ip'] ) OR filter_var( $info['review_ip'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['review_ip'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['review_edit_time'] ) )
+ {
+ if ( $info['review_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['review_edit_time'] = $info['review_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_edit_time'] = 0;
+ }
+
+ if ( !isset( $info['review_edit_name'] ) )
+ {
+ $info['review_edit_name'] = '';
+ }
+
+ if ( !isset( $info['review_append_edit'] ) )
+ {
+ $info['review_append_edit'] = 0;
+ }
+
+ if ( isset( $info['review_votes_data'] ) )
+ {
+ if ( !is_array( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_data'] = json_decode( $info['review_votes_data'], TRUE );
+ }
+
+ $newVoters = array();
+ if ( !is_null( $info['review_votes_data'] ) AND count( $info['review_votes_data'] ) )
+ {
+ foreach( $info['review_votes_data'] as $member => $vote )
+ {
+ try
+ {
+ $memberId = $this->software->app->getLink( $member, 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ $newVoters[ $memberId ] = $vote;
+ }
+ }
+
+ if ( count( $newVoters ) )
+ {
+ $info['review_votes_data'] = json_encode( $newVoters );
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+
+ if ( !isset( $info['review_votes'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes'] = 0;
+ }
+ else
+ {
+ $info['review_votes'] = count( json_decode( $info['review_votes_data'], TRUE ) );
+ }
+ }
+
+ if ( !isset( $info['review_votes_helpful'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_helpful'] = 0;
+ }
+ else
+ {
+ $helpful = 0;
+ foreach( json_decode( $info['review_votes_data'], TRUE ) AS $member => $vote )
+ {
+ if ( $vote == 1 )
+ {
+ $helpful += 1;
+ }
+ }
+
+ $info['review_votes_helpful'] = $helpful;
+ }
+ }
+
+ if ( !isset( $info['review_approved'] ) )
+ {
+ $info['review_approved'] = 1;
+ }
+
+ if ( !isset( $info['review_version'] ) )
+ {
+ $info['review_version'] = NULL;
+ }
+
+ $id = $info['review_id'];
+ unset( $info['review_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'downloads_reviews', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'downloads_reviews' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Map Data to insert
+ * @param string|NULL $filepath Path to the file, or NULL.
+ * @param string|NULL $filedata Binary data for the file, or NULL.
+ * @return integer|boolean The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ $map['location_key'] = 'downloads_Downloads';
+ $map['id1_type'] = 'downloads_files';
+ $map['id1_from_parent'] = FALSE;
+ $map['id2_from_parent'] = FALSE;
+ /* Some set up */
+ if ( !isset( $info['id3'] ) )
+ {
+ $info['id3'] = NULL;
+ }
+
+ if ( is_null( $info['id3'] ) OR $info['id3'] != 'review' )
+ {
+ $map['id2_type'] = 'downloads_comments';
+ }
+ else
+ {
+ $map['id2_type'] = 'downloads_reviews';
+ }
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Forums.php b/applications/convert/sources/Library/Forums.php
new file mode 100644
index 00000000..e49b705f
--- /dev/null
+++ b/applications/convert/sources/Library/Forums.php
@@ -0,0 +1,1974 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+/**
+ * Inherit from the Core Library so we can access or overload methods as needed (for example, we do not need to redeclare convertAttachments again here, but we can overload to do extra things if needed).
+ */
+class _Forums extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'forums';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertForumsForums':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_forums',
+ 'step_method' => 'convertForumsForums',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_forums' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'forums_forums',
+ );
+ break;
+
+ case 'convertForumsTopics':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_topics',
+ 'step_method' => 'convertForumsTopics',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_topics' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertForumsForums' ),
+ 'link_type' => 'forums_topics',
+ );
+ break;
+
+ case 'convertForumsPosts':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_posts',
+ 'step_method' => 'convertForumsPosts',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_posts' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertForumsTopics' ),
+ 'link_type' => 'forums_posts',
+ );
+ break;
+
+ case 'convertForumsArchivePosts':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_archive_posts',
+ 'step_method' => 'convertForumsArchivePosts',
+ 'ips_rows' => \IPS\forums\Topic\ArchivedPost::db()->select( 'COUNT(*)', 'forums_archive_posts' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertForumsTopics' ),
+ 'link_type' => 'forums_posts',
+ );
+ break;
+
+ case 'convertForumsTopicMultimods':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_topic_multimods',
+ 'step_method' => 'convertForumsTopicMultimods',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_topic_mmod' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertForumsForums' ),
+ 'link_type' => 'forums_topic_mmod',
+ );
+ break;
+
+ case 'convertForumsRssImports':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_rss_imports',
+ 'step_method' => 'convertForumsRssImports',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_rss_import' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertForumsForums' ),
+ 'link_type' => 'forums_rss_import',
+ );
+ break;
+
+ case 'convertForumsRssImported':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_rss_imported',
+ 'step_method' => 'convertForumsRssImported',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_rss_imported' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertForumsTopics', 'convertForumsRssImports' ),
+ 'link_type' => 'forums_rss_imported',
+ );
+ break;
+
+ case 'convertForumsQuestionRatings':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_question_ratings',
+ 'step_method' => 'convertForumsQuestionRatings',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_question_ratings' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertForumsTopics' ),
+ 'link_type' => 'forums_question_ratings',
+ );
+ break;
+
+ case 'convertForumsAnswerRatings':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_answer_ratings',
+ 'step_method' => 'convertForumsAnswerRatings',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'forums_answer_ratings' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertForumsPosts' ),
+ 'link_type' => 'forums_answer_ratings',
+ );
+ break;
+
+ case 'convertAttachments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_forums_attachments',
+ 'step_method' => 'convertAttachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( "location_key=?", 'forums_Forums' ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 50,
+ 'dependencies' => array( 'convertForumsPosts' ),
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Get method from menu rows - abstracted to allow 'fake' entries not in menuRows()
+ *
+ * @param string $method Method requested
+ * @return array
+ */
+ public function getMethodFromMenuRows( $method )
+ {
+ if( $method == 'convertForumsBoards' )
+ {
+ $method = 'convertForumsForums';
+ }
+
+ return parent::getMethodFromMenuRows( $method );
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertForumsForums':
+ $return['convertForumsForums'] = array(
+ 'core_follow' => array( 'follow_app=? AND follow_area=?', 'forums', 'forum' ),
+ 'forums_forums' => NULL
+ );
+ break;
+
+ case 'convertForumsTopics':
+ $return['convertForumsTopics'] = array(
+ 'core_follow' => array( 'follow_app=? AND follow_area=?', 'forums', 'topic' ),
+ 'core_polls' => NULL,
+ 'core_ratings' => array( 'class=?', 'IPS\\forums\\Topic\\Post' ),
+ 'core_tags' => array( 'tag_meta_app=? AND tag_meta_area=?', 'forums', 'forums' ),
+ 'core_voters' => NULL,
+ 'forums_question_ratings' => NULL,
+ 'forums_topics' => NULL
+ );
+ break;
+
+ case 'convertForumsPosts':
+ $return['convertForumsPosts'] = array(
+ 'core_edit_history' => array( 'class=?', 'IPS\\forums\\Topic\\Post' ),
+ 'core_reputation_index' => array( 'app=? AND type=?', 'forums', 'pid' ),
+ 'forums_answer_ratings' => NULL,
+ 'forums_posts' => NULL
+ );
+ break;
+
+ case 'convertForumsArchivePosts':
+ $return['convertForumsArchivePosts'] = array( 'forums_archive_posts' => NULL );
+ break;
+
+ case 'convertForumsTopicMultimods':
+ $return['convertForumsTopicMultimods'] = array( 'forums_topic_mmod' => NULL );
+ break;
+
+ case 'convertForumsRssImports':
+ $return['convertForumsRssImports'] = array( 'forums_rss_imports' => NULL );
+ break;
+
+ case 'convertForumsRssImported':
+ $retun['convertForumsRssImported'] = array( 'forums_rss_imported' => NULL );
+ break;
+
+ case 'convertForumsQuestionRatings':
+ $retun['convertForumsQuestionRatings'] = array( 'forums_question_ratings' => NULL );
+ break;
+
+ case 'convertForumsAnswerRatings':
+ $return['convertForumsAnswerRatings'] = array( 'forums_answer_ratings' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $attachIds = array();
+ foreach( \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( 'location_key=?', 'forums_Forums' ) ) AS $attachment )
+ {
+ $attachIds[] = $attachment;
+ }
+ $return['convertAttachments'] = array( 'core_attachments' => \IPS\Db::i()->in( 'attach_id', $attachIds ), 'core_attachments_map' => array( "location_key=?", 'forums_Forums' ) );
+ break;
+ }
+ }
+ return $return[$method];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a Forum
+ *
+ * @param array $info Data to insert
+ * @param array|NULL $perms Permissions or NULL to insert no permissions.
+ * @param string|NULL $iconpath The path to the forums icon, or NULL.
+ * @param string|NULL $icondata The binary data for the forums icon, or NULL.
+ * @return integer|boolean The ID of the newly inserted forum, or FALSE on failure.
+ */
+ public function convertForumsForum( $info=array(), $perms=NULL, $iconpath=NULL, $icondata=NULL )
+ {
+ if ( !isset( $info['id'] ) )
+ {
+ $this->software->app->log( 'forums_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['name'] ) )
+ {
+ $name = "Untitled Forum {$info['id']}";
+ $this->software->app->log( 'forum_missing_name', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['id'] );
+ }
+ else
+ {
+ $name = $info['name'];
+ unset( $info['name'] );
+ }
+
+ if ( !array_key_exists( 'description', $info ) )
+ {
+ $desc = '';
+ }
+ else
+ {
+ /* Make sure the description isn't empty/null */
+ $desc = $info['description'] ?: '';
+ unset( $info['description'] );
+ }
+
+ if ( !isset( $info['topics'] ) )
+ {
+ $info['topics'] = 0;
+ }
+
+ if ( !isset( $info['posts'] ) )
+ {
+ $info['posts'] = 0;
+ }
+
+ if ( isset( $info['last_post'] ) )
+ {
+ if ( $info['last_post'] instanceof \IPS\DateTime )
+ {
+ $info['last_post'] = $info['last_post']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_post'] = NULL;
+ }
+
+ if ( isset( $info['last_poster_id'] ) )
+ {
+ try
+ {
+ $info['last_poster_id'] = $this->software->app->getLink( $info['last_poster_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['last_poster_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['last_poster_id'] = 0;
+ }
+
+ if ( !isset( $info['last_poster_name'] ) )
+ {
+ $info['last_poster_name'] = '';
+ }
+
+ /* Do parent_id now - we'll need it later */
+ if ( isset( $info['parent_id'] ) )
+ {
+ if ( $info['parent_id'] != -1 )
+ {
+ try
+ {
+ $info['parent_id'] = $this->software->app->getLink( $info['parent_id'], 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Does the ID exist in the source? */
+ $info['conv_parent'] = $info['parent_id'];
+
+ /* Default to a category unless a later converted forum updates the parent_id */
+ $info['parent_id'] = -1;
+ }
+ }
+ }
+ else
+ {
+ $info['parent_id'] = $this->_getOrphanedForumCategory();
+ }
+
+ if ( !isset( $info['position'] ) )
+ {
+ try
+ {
+ $position = \IPS\Db::i()->select( 'MAX(position)', 'forums_forums', array( "parent_id=?", $info['parent_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $position = 0;
+ }
+
+ $info['position'] = $position + 1;
+ }
+
+ if ( !isset( $info['password'] ) )
+ {
+ $info['password'] = NULL;
+ }
+
+ if ( isset( $info['password_override'] ) )
+ {
+ /* Array? */
+ if ( !is_array( $info['password_override'] ) )
+ {
+ $info['password_override'] = explode( ',', $info['password_override'] );
+ }
+
+ $passwordGroupsToSave = array();
+ foreach( $info['password_override'] AS $group )
+ {
+ try
+ {
+ $passwordGroupsToSave[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+ $info['password_override'] = implode( ',', $passwordGroupsToSave );
+ }
+ else
+ {
+ $info['password_override'] = '';
+ }
+
+ if ( !isset( $info['last_title'] ) )
+ {
+ $info['last_title'] = NULL;
+ }
+
+ /* We can't know this yet */
+ $info['last_id'] = 0;
+
+ if ( !isset( $info['sort_key'] ) )
+ {
+ $info['sort_key'] = 'last_post';
+ }
+
+ if ( !isset( $info['show_rules'] ) )
+ {
+ $info['show_rules'] = 0;
+ }
+
+ if ( !isset( $info['preview_posts'] ) )
+ {
+ $info['preview_posts'] = 0;
+ }
+
+ if ( !isset( $info['allow_poll'] ) )
+ {
+ $info['allow_poll'] = 1;
+ }
+
+ if ( !isset( $info['inc_postcount'] ) )
+ {
+ $info['inc_postcount'] = 1;
+ }
+
+ /* Can't convert themes */
+ $info['skin_id'] = 0;
+
+ if ( !isset( $info['redirect_url'] ) OR filter_var( $info['redirect_url'], FILTER_VALIDATE_URL ) === FALSE )
+ {
+ $info['redirect_url'] = NULL;
+ }
+
+ if ( !isset( $info['redirect_on'] ) )
+ {
+ if ( !is_null( $info['redirect_url'] ) )
+ {
+ $info['redirect_on'] = 1;
+ }
+ else
+ {
+ $info['redirect_on'] = 0;
+ }
+ }
+
+ if ( !isset( $info['redirect_hits'] ) )
+ {
+ $info['redirect_hits'] = 0;
+ }
+
+ if ( !isset( $info['sub_can_post'] ) )
+ {
+ if ( $info['parent_id'] == -1 )
+ {
+ $info['sub_can_post'] = 0;
+ }
+ else
+ {
+ $info['sub_can_post'] = 1;
+ }
+ }
+
+ if ( !isset( $info['permission_showtopic'] ) )
+ {
+ $info['permission_showtopic'] = 0;
+ }
+
+ if ( !isset( $info['queued_topics'] ) )
+ {
+ $info['queued_topics'] = 0;
+ }
+
+ if ( !isset( $info['queued_posts'] ) )
+ {
+ $info['queued_posts'] = 0;
+ }
+
+ if ( !isset( $info['forum_allow_rating'] ) )
+ {
+ $info['forum_allow_rating'] = 0;
+ }
+
+ if ( !isset( $info['min_posts_post'] ) )
+ {
+ $info['min_posts_post'] = 0;
+ }
+
+ if ( !isset( $info['min_posts_view'] ) )
+ {
+ $info['min_posts_view'] = 0;
+ }
+
+ if ( !isset( $info['can_view_others'] ) )
+ {
+ $info['can_view_others'] = 1;
+ }
+
+ /* Just reset this */
+ $info['name_seo'] = \IPS\Http\url::seoTitle( $name );
+
+ if ( !isset( $info['seo_last_title'] ) )
+ {
+ if ( !is_null( $info['last_title'] ) )
+ {
+ $info['seo_last_title'] = \IPS\Http\Url::seoTitle( $info['last_title'] );
+ }
+ else
+ {
+ $info['seo_last_title'] = '';
+ }
+ }
+
+ if ( !isset( $info['seo_last_name'] ) )
+ {
+ if ( !is_null( $info['last_poster_name'] ) )
+ {
+ $info['seo_last_name'] = \IPS\Http\Url::seoTitle( $info['last_poster_name'] );
+ }
+ else
+ {
+ $info['seo_last_name'] = '';
+ }
+ }
+
+ /* No longer used */
+ $info['last_x_topic_ids'] = NULL;
+
+ $bitoptions = 0;
+ if ( isset( $info['forums_bitoptions'] ) )
+ {
+ foreach( \IPS\forums\Forum::$bitOptions['forums_bitoptions']['forums_bitoptions'] AS $key => $value )
+ {
+ if ( isset( $info['forums_bitoptions'][$key] ) AND $info['forums_bitoptions'][$key] )
+ {
+ $bitoptions += $value;
+ }
+ }
+ $info['forums_bitoptions'] = $bitoptions;
+ }
+
+ if ( !isset( $info['disable_sharelinks'] ) )
+ {
+ $info['disable_sharelinks'] = 0;
+ }
+
+ if ( !isset( $info['tag_predefined'] ) )
+ {
+ $info['tag_predefined'] = NULL;
+ }
+
+ if ( !isset( $info['archived_topics'] ) )
+ {
+ $info['archived_topics'] = 0;
+ }
+
+ if ( !isset( $info['archived_posts'] ) )
+ {
+ $info['archived_posts'] = 0;
+ }
+
+ if ( !isset( $info['ipseo_priority'] ) )
+ {
+ $info['ipseo_priority'] = -1;
+ }
+
+ if ( !isset( $info['viglink'] ) )
+ {
+ $info['viglink'] = 1;
+ }
+
+ if ( isset( $info['qa_rate_questions'] ) )
+ {
+ if ( $info['qa_rate_questions'] != '*' )
+ {
+ if ( !is_array( $info['qa_rate_questions'] ) )
+ {
+ $info['qa_rate_questions'] = explode( ',', $info['qa_rate_questions'] );
+ }
+
+ if ( count( $info['qa_rate_questions'] ) )
+ {
+ $qaQuestionGroups = array();
+ foreach( $info['qa_rate_questions'] AS $group )
+ {
+ try
+ {
+ $qaQuestionGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+
+ if ( count( $qaQuestionGroups ) )
+ {
+ $info['qa_rate_questions'] = implode( ',', $qaQuestionGroups );
+ }
+ else
+ {
+ $info['qa_rate_questions'] = '*';
+ }
+ }
+ else
+ {
+ $info['qa_rate_questions'] = '*';
+ }
+ }
+ }
+ else
+ {
+ $info['qa_rate_questions'] = '*';
+ }
+
+ if ( isset( $info['qa_rate_answers'] ) )
+ {
+ if ( $info['qa_rate_answers'] != '*' )
+ {
+ if ( !is_array( $info['qa_rate_answers'] ) )
+ {
+ $info['qa_rate_answers'] = explode( ',', $info['qa_rate_answers'] );
+ }
+
+ if ( count( $info['qa_rate_anwsers'] ) )
+ {
+ $qaAnswerGroups = array();
+ foreach( $info['qa_rate_answers'] AS $group )
+ {
+ try
+ {
+ $qaAnswerGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+
+ if ( count( $qaAnswerGroups ) )
+ {
+ $info['qa_rate_answers'] = implode( ',', $qaAnswerGroups );
+ }
+ else
+ {
+ $info['qa_rate_answers'] = '*';
+ }
+ }
+ else
+ {
+ $info['qa_rate_answers'] = '*';
+ }
+ }
+ }
+ else
+ {
+ $info['qa_rate_answers'] = '*';
+ }
+
+ try
+ {
+ if ( isset( $info['icon'] ) AND ( !is_null( $iconpath ) OR !is_null( $icondata ) ) )
+ {
+ if ( is_null( $icondata ) AND !is_null( $iconpath ) )
+ {
+ $icondata = file_get_contents( $iconpath );
+ }
+ $file = \IPS\File::create( 'forums_Icons', $info['icon'], $filedata );
+ $info['icon'] = (string) $file;
+ }
+ else
+ {
+ $info['icon'] = NULL;
+ }
+ }
+ catch( \Exception $e )
+ {
+ $info['icon'] = NULL;
+ }
+ catch( \ErrorException $e )
+ {
+ $info['icon'] = NULL;
+ }
+
+ $id = $info['id'];
+ unset( $info['id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_forums', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'forums_forums' );
+
+ \IPS\Lang::saveCustom( 'forums', "forums_forum_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'forums', "forums_forum_{$inserted_id}_desc", $desc );
+
+ \IPS\Db::i()->update( 'forums_forums', array( "parent_id" => $inserted_id ), array( "conv_parent=? AND conv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Get Parent Forum ID for Orphaned Forums
+ *
+ * @return integer The ID of the forum created for forums that do not have a parent forum.
+ */
+ protected function _getOrphanedForumCategory()
+ {
+ try
+ {
+ $id = $this->software->app->getLink( '__orphaned__', 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $id = $this->convertForumsForum( array(
+ 'id' => '__orphaned__',
+ 'name' => "Converted Forums",
+ 'parent_id' => -1,
+ ) );
+ }
+
+ return $id;
+ }
+
+ /**
+ * Convert a topic
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted topic, or FALSE on failure
+ */
+ public function convertForumsTopic( $info=array() )
+ {
+ if ( !isset( $info['tid'] ) )
+ {
+ $this->software->app->log( 'topic_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['title'] ) )
+ {
+ $info['title'] = "Untitled Topic {$info['tid']}";
+ $this->software->app->log( 'topic_missing_title', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ }
+
+ if ( isset( $info['forum_id'] ) )
+ {
+ try
+ {
+ $info['forum_id'] = $this->software->app->getLink( $info['forum_id'], 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'topic_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['tid'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'topic_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['tid'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['state'] ) OR !in_array( $info['state'], array( 'open', 'closed' ) ) )
+ {
+ $info['state'] = 'open';
+ }
+
+ if ( !isset( $info['posts'] ) )
+ {
+ $info['posts'] = 0;
+ }
+
+ if ( isset( $info['starter_id'] ) )
+ {
+ try
+ {
+ $info['starter_id'] = $this->software->app->getLink( $info['starter_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['starter_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['starter_id'] = 0;
+ }
+
+ if ( isset( $info['start_date'] ) )
+ {
+ if ( $info['start_date'] instanceof \IPS\DateTime )
+ {
+ $info['start_date'] = $info['start_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['start_date'] = time();
+ }
+
+ if ( isset( $info['last_poster_id'] ) )
+ {
+ try
+ {
+ $info['last_poster_id'] = $this->software->app->getLink( $info['last_poster_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['last_poster_id'] = $info['starter_id'];
+ }
+ }
+ else
+ {
+ $info['last_poster_id'] = $info['starter_id'];
+ }
+
+ if ( isset( $info['last_post'] ) )
+ {
+ if ( $info['last_post'] instanceof \IPS\DateTime )
+ {
+ $info['last_post'] = $info['last_post']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_post'] = $info['start_date'];
+ }
+
+ if ( !isset( $info['starter_name'] ) )
+ {
+ $starter = \IPS\Member::load( $info['starter_id'] );
+
+ if ( $starter->member_id )
+ {
+ $info['starter_name'] = $starter->name;
+ }
+ else
+ {
+ $info['starter_name'] = NULL;
+ }
+ }
+
+ if ( !isset( $info['last_poster_name'] ) )
+ {
+ $last = \IPS\Member::load( $info['last_poster_id'] );
+
+ if ( $last->member_id )
+ {
+ $info['last_poster_name'] = $last->name;
+ }
+ else
+ {
+ $info['last_poster_name'] = $info['starter_name'];
+ }
+ }
+
+ /* Polls - pass off to the core library. Unlike ratings and follows, we need to do this here as we need to know the Poll ID. */
+ if ( isset( $info['poll_state'] ) AND is_array( $info['poll_state'] ) )
+ {
+ if ( $info['poll_state'] = $this->convertPoll( $info['poll_state']['poll_data'], $info['poll_state']['vote_data'] ) )
+ {
+ if ( !isset( $info['last_vote'] ) )
+ {
+ try
+ {
+ $lastVote = \IPS\Db::i()->select( 'vote_date', 'core_voters', array( "poll=?", $info['poll_state'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $lastVote = time();
+ }
+
+ $info['last_vote'] = $lastVote;
+ }
+ }
+ else
+ {
+ $info['poll_state'] = NULL;
+ $info['last_vote'] = NULL;
+ }
+ }
+ else
+ {
+ $info['poll_state'] = NULL;
+ $info['last_vote'] = NULL;
+ }
+
+ if ( !isset( $info['views'] ) )
+ {
+ $info['views'] = 0;
+ }
+
+ if ( !isset( $info['approved'] ) )
+ {
+ $info['approved'] = 1;
+ }
+
+ /* Not Used */
+ $info['author_mode'] = 0;
+
+ if ( !isset( $info['pinned'] ) )
+ {
+ $info['pinned'] = 0;
+ }
+
+ if ( isset( $info['moved_to'] ) )
+ {
+ if ( !is_array( $info['moved_to'] ) )
+ {
+ list( $topic, $forum ) = explode( '&', $info['moved_to'] );
+ }
+ else
+ {
+ list( $topic, $forum ) = $info['moved_to'];
+ }
+
+ try
+ {
+ $topic = $this->software->app->getLink( $topic, 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $topic = NULL;
+ }
+
+ try
+ {
+ $forum = $this->software->app->getLink( $forum, 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $forum = NULL;
+ }
+
+ if ( is_null( $topic ) OR is_null( $forum ) )
+ {
+ $info['moved_to'] = NULL;
+ }
+ else
+ {
+ $info['moved_to'] = $topic . '&' . $forum;
+ }
+ }
+ else
+ {
+ $info['moved_to'] = NULL;
+ }
+
+ /* Can't know this */
+ $info['topic_firstpost'] = 0;
+
+ if ( !isset( $info['topic_queuedposts'] ) )
+ {
+ $info['topic_queuedposts'] = 0;
+ }
+
+ if ( isset( $info['topic_open_time'] ) )
+ {
+ if ( $info['topic_open_time'] instanceof \IPS\DateTime )
+ {
+ $info['topic_open_time'] = $info['topic_open_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['topic_open_time'] = 0;
+ }
+
+ if ( isset( $info['topic_close_time'] ) )
+ {
+ if ( $info['topic_close_time'] instanceof \IPS\DateTime )
+ {
+ $info['topic_close_time'] = $info['topic_close_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['topic_close_time'] = 0;
+ }
+
+ if ( !isset( $info['topic_rating_total'] ) )
+ {
+ $info['topic_rating_total'] = 0;
+ }
+
+ if ( !isset( $info['topic_rating_hits'] ) )
+ {
+ $info['topic_rating_hits'] = 0;
+ }
+
+ if ( !isset( $info['title_seo'] ) )
+ {
+ $info['title_seo'] = \IPS\Http\Url::seoTitle( $info['title'] );
+ }
+
+ if ( isset( $info['moved_on'] ) )
+ {
+ if ( $info['moved_on'] instanceof \IPS\DateTime )
+ {
+ $info['moved_on'] = $info['moved_on']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['moved_on'] = 0;
+ }
+
+ if ( !isset( $info['topic_archive_status'] ) )
+ {
+ $info['topic_archive_status'] = 0;
+ }
+
+ if ( isset( $info['last_real_post'] ) )
+ {
+ if ( $info['last_real_post'] instanceof \IPS\DateTime )
+ {
+ $info['last_real_post'] = $info['last_real_post']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['last_real_post'] = $info['last_post'];
+ }
+
+ /* Can't knew these */
+ $info['topic_answered_pid'] = 0;
+ $info['popular_time'] = 0;
+
+ if ( !isset( $info['featured'] ) )
+ {
+ $info['featured'] = 0;
+ }
+
+ if ( !isset( $info['question_rating'] ) )
+ {
+ $info['question_rating'] = NULL;
+ }
+
+ if ( !isset( $info['topic_hiddenposts'] ) )
+ {
+ $info['topic_hiddenposts'] = 0;
+ }
+
+ $id = $info['tid'];
+ unset( $info['tid'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_topics', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'forums_topics' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a Forum Post
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted post, or FALSE on failure.
+ */
+ public function convertForumsPost( $info=array() )
+ {
+ if ( !isset( $info['pid'] ) )
+ {
+ $this->software->app->log( 'post_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['topic_id'] ) )
+ {
+ try
+ {
+ $info['topic_id'] = $this->software->app->getLink( $info['topic_id'], 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'post_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['pid'] );
+ return FALSE;
+ }
+ }
+ elseif( isset( $info['conv_topic_id'] ) AND $info['conv_topic_id'] > 0 )
+ {
+ $info['topic_id'] = $info['conv_topic_id'];
+ unset( $info['conv_topic_id'] );
+ }
+ else
+ {
+ $this->software->app->log( 'post_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['pid'] );
+ return FALSE;
+ }
+
+ if ( empty( $info['post'] ) )
+ {
+ $this->software->app->log( 'post_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['pid'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['append_edit'] ) )
+ {
+ $info['append_edit'] = 0;
+ }
+
+ if ( isset( $info['edit_time'] ) )
+ {
+ if ( $info['edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['edit_time'] = $info['edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['edit_time'] = NULL;
+ }
+
+ if ( isset( $info['author_id'] ) )
+ {
+ try
+ {
+ $info['author_id'] = $this->software->app->getLink( $info['author_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['author_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['author_id'] = 0;
+ }
+
+ if ( !isset( $info['author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['author_id'] );
+
+ if ( $author->member_id )
+ {
+ $info['author_name'] = $author->name;
+ }
+ }
+
+ if ( !isset( $info['ip_address'] ) OR filter_var( $info['ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['post_date'] ) )
+ {
+ if ( $info['post_date'] instanceof \IPS\DateTime )
+ {
+ $info['post_date'] = $info['post_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['post_date'] = time();
+ }
+
+ if ( !isset( $info['queued'] ) )
+ {
+ $info['queued'] = 0;
+ }
+
+ if ( !isset( $info['new_topic'] ) )
+ {
+ $info['new_topic'] = 0;
+ }
+
+ if ( !isset( $info['edit_name'] ) )
+ {
+ $info['edit_name'] = NULL;
+ }
+
+ /* Meh */
+ $info['post_key'] = 0;
+
+ if ( !isset( $info['post_htmlstate'] ) )
+ {
+ $info['post_htmlstate'] = 0;
+ }
+
+ if ( !isset( $info['post_edit_reason'] ) )
+ {
+ $info['post_edit_reason'] = '';
+ }
+
+ /* The Bit Options contain where or not this post is marked as best answer. Set a flag accordingly so we can update the topic later. */
+ $isBestAnswer = FALSE;
+ $bitoptions = 0;
+ if ( isset( $info['post_bwoptions'] ) )
+ {
+ foreach( \IPS\forums\Topic\Post::$bitOptions['post_bwoptions']['post_bwoptions'] AS $key => $value )
+ {
+ if ( isset( $info['post_bwoptions'][$key] ) AND $info['post_bwoptions'][$key] )
+ {
+ $bitoptions += $value;
+ if ( $key === 'best_answer' )
+ {
+ $isBestAnswer = TRUE;
+ }
+ }
+ }
+ $info['post_bwoptions'] = $bitoptions;
+ }
+
+ if ( isset( $info['pdelete_time'] ) )
+ {
+ if ( $info['pdelete_time'] instanceof \IPS\DateTime )
+ {
+ $info['pdelete_time'] = $info['pdelete_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['pdelete_time'] = 0;
+ }
+
+ /* Are these even used yet? */
+ if ( !isset( $info['post_field_int'] ) )
+ {
+ $info['post_field_int'] = 0;
+ }
+
+ if ( !isset( $info['post_field_t1'] ) )
+ {
+ $info['post_field_t1'] = NULL;
+ }
+
+ if ( !isset( $info['post_field_t2'] ) )
+ {
+ $info['post_field_t2'] = NULL;
+ }
+
+ $id = $info['pid'];
+ unset( $info['pid'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_posts', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'forums_posts' );
+
+ $topicUpdate = array();
+
+ if ( $info['new_topic'] == 1 )
+ {
+ $topicUpdate['topic_firstpost'] = $inserted_id;
+ }
+
+ if ( $isBestAnswer == TRUE )
+ {
+ $topicUpdate['topic_answered_pid'] = $inserted_id;
+ }
+
+ if ( count( $topicUpdate ) )
+ {
+ \IPS\Db::i()->update( 'forums_topics', $topicUpdate, array( "tid=?", $info['topic_id'] ) );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Archived Post
+ *
+ * @param array $info Data to insert
+ * @param integer|boolean The ID of the newly inserted Archived Post, or FALSE on failure.
+ */
+ public function convertForumsArchivedPost( $info=array() )
+ {
+ /* This works a little differently... we actually need to insert it into the main posts table first, then move it. As such, the $info array should match that of the main posts method EXCEPT for a few keys which we'll extract out. */
+ if ( isset( $info['archive_added'] ) )
+ {
+ $added = $info['archive_added'];
+ unset( $info['archive_added'] );
+ }
+ else
+ {
+ $added = time();
+ }
+
+ $return = $this->convertForumsPost( $info );
+
+ /* If $return is FALSE, stop here. Something went wrong. */
+ if ( $return === FALSE )
+ {
+ return $return;
+ }
+
+ /* Re-extract the post. We need the topic too. */
+ try
+ {
+ $post = \IPS\Db::i()->select( '*', 'forums_posts', array( "pid=?", $return ) )->first();
+ $topic = \IPS\Db::i()->select( '*', 'forums_topics', array( "tid=?", $post['topic_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Um... something really bad happened. */
+ $this->software->app->log( 'forums_archive_posts_omgonoes', __METHOD__, \IPS\convert\App::LOG_ERROR, $return );
+ throw new \IPS\convert\Exception;
+ }
+
+ /* Remap everything so it can fit into the archive table */
+ $archive = array(
+ 'archive_id' => $post['pid'],
+ 'archive_author_id' => $post['author_id'],
+ 'archive_author_name' => $post['author_name'],
+ 'archive_ip_address' => $post['ip_address'],
+ 'archive_content_date' => $post['post_date'],
+ 'archive_content' => $post['post'],
+ 'archive_queued' => $post['queued'],
+ 'archive_topic_id' => $post['topic_id'],
+ 'archive_is_first' => $post['new_topic'],
+ 'archive_bwoptions' => $post['post_bwoptions'],
+ 'archive_attach_key' => $post['post_key'],
+ 'archive_html_mode' => $post['post_htmlstate'],
+ 'archive_show_edited_by' => $post['append_edit'],
+ 'archive_edit_time' => $post['edit_time'],
+ 'archive_edit_name' => $post['edit_name'],
+ 'archive_edit_reason' => $post['edit_reason'] ?: '',
+ 'archive_added' => $added,
+ 'archive_restored' => 0,
+ 'archive_forum_id' => $topic['forum_id'],
+ 'archive_field_int' => $post['post_field_int']
+ );
+
+ \IPS\forums\Topic\ArchivedPost::db()->insert( 'forums_archive_posts', $archive );
+ \IPS\Db::i()->delete( 'forums_posts', array( "pid=?", $archive['archive_id'] ) );
+
+ return $archive['archive_id'];
+ }
+
+ /**
+ * Convert a forums topic multimod
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted Multi Mod, or FALSE on failure.
+ */
+ public function convertForumsTopicMultimod( $info=array() )
+ {
+ /* Another instance where we do not necessarily need this, but will store anyway if we have it */
+ $hasId = TRUE;
+ if ( !isset( $info['mm_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( !isset( $info['mm_name'] ) )
+ {
+ $name = "Untitled Multi-Mod";
+ $this->software->app->log( 'forums_topic_mmod_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE, ( $hasId ) ? $info['mm_id'] : NULL );
+ }
+ else
+ {
+ $name = $info['mm_name'];
+ unset( $info['mm_name'] );
+ }
+
+ if ( !isset( $info['mm_enabled'] ) )
+ {
+ $info['mm_enabled'] = 1;
+ }
+
+ if ( !isset( $info['topic_state'] ) OR !in_array( $info['topic_state'], array( 'leave', 'open', 'close' ) ) )
+ {
+ $info['topic_state'] = 'leave';
+ }
+
+ if ( !isset( $info['topic_pin'] ) OR !in_array( $info['topic_pin'], array( 'leave', 'pin', 'unpin' ) ) )
+ {
+ $info['topic_pin'] = 'leave';
+ }
+
+ if ( isset( $info['topic_move'] ) )
+ {
+ try
+ {
+ $info['topic_move'] = $this->software->app->getLink( $info['topic_move'], 'forums_forums' );
+
+ if ( !isset( $info['topic_move_link'] ) )
+ {
+ $info['topic_move_link'] = 0;
+ }
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['topic_move'] = 0;
+ $info['topic_move_link'] = 0;
+ }
+ }
+ else
+ {
+ $info['topic_move'] = 0;
+ $info['topic_move_link'] = 0;
+ }
+
+ if ( !isset( $info['topic_title_st'] ) )
+ {
+ $info['topic_title_st'] = '';
+ }
+
+ if ( !isset( $info['topic_title_end'] ) )
+ {
+ $info['topic_title_end'] = '';
+ }
+
+ if ( isset( $info['topic_reply'] ) )
+ {
+ if ( empty( $info['topic_reply_content'] ) )
+ {
+ $info['topic_reply'] = 0;
+ $info['topic_reply_content'] = NULL; # empty account for !isset so make sure it's actually set
+ }
+ else
+ {
+ $info['topic_reply'] = 1;
+ }
+ }
+ else
+ {
+ $info['topic_reply'] = 0;
+ $info['topic_reply_content'] = NULL;
+ }
+
+ if ( !isset( $info['topic_reply_postcontent'] ) )
+ {
+ $info['topic_reply_postcount'] = 1;
+ }
+
+ if ( isset( $info['mm_forums'] ) )
+ {
+ if ( $info['mm_forums'] != '*' )
+ {
+ if ( !is_array( $info['mm_forums'] ) )
+ {
+ $info['mm_forums'] = explode( ',', $info['mm_forums'] );
+ }
+
+ $forums = array();
+ if ( count( $info['mm_forums'] ) )
+ {
+ foreach( $info['mm_forums'] AS $forum )
+ {
+ try
+ {
+ $forums[] = $this->software->app->getLink( $forum, 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+ }
+
+ if ( count( $forums ) )
+ {
+ $info['mm_forums'] = implode( ',', $forums );
+ }
+ else
+ {
+ $info['mm_forums'] = '*';
+ }
+ }
+ }
+ else
+ {
+ $info['mm_forums'] = '*';
+ }
+
+ if ( !isset( $info['topic_approve'] ) OR !in_array( $info['topic_approve'], array( 0, 1, 2 ) ) )
+ {
+ $info['topic_approve'] = 0;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['mm_id'];
+ unset( $info['mm_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_topic_mmod', $info );
+ \IPS\Lang::saveCustom( 'forums', "forums_mmod_{$inserted_id}", $name );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'forums_topic_multimod' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an RSS Import Feed
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted import feed, or FALSE on failure.
+ */
+ public function convertForumsRssImport( $info=array() )
+ {
+ if ( !isset( $info['rss_import_id'] ) )
+ {
+ $this->software->app->log( 'rss_import_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* Required Stuff */
+ if ( !isset( $info['rss_import_url'] ) OR filter_var( $info['rss_import_url'], FILTER_VALIDATE_URL ) === FALSE )
+ {
+ $this->software->app->log( 'rss_import_invalid_url', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_import_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['rss_import_forum_id'] ) )
+ {
+ try
+ {
+ $info['rss_import_forum_id'] = $this->software->app->getLink( $info['rss_import_forum_id'], 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rss_import_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_import_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'rss_import_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_import_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['rss_import_mid'] ) )
+ {
+ try
+ {
+ $info['rss_import_mid'] = $this->software->app->getLink( $info['rss_import_mid'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rss_import_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_import_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'rss_import_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['rss_import_id'] );
+ return FALSE;
+ }
+
+ /* Stuff we can use defaults for */
+ if ( !isset( $info['rss_import_title'] ) )
+ {
+ $info['rss_import_title'] = "Untitled Feed {$info['rss_import_id']}";
+ }
+
+ if ( !isset( $info['rss_import_pergo'] ) )
+ {
+ $info['rss_import_pergo'] = 10;
+ }
+
+ if ( !isset( $info['rss_import_time'] ) )
+ {
+ $info['rss_import_time'] = 30;
+ }
+
+ if ( isset( $info['rss_import_last_import'] ) )
+ {
+ if ( $info['rss_import_last_import'] instanceof \IPS\DateTime )
+ {
+ $info['rss_import_last_import'] = $info['rss_import_last_import']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['rss_import_last_import'] = time();
+ }
+
+ if ( !isset( $info['rss_import_showlink'] ) )
+ {
+ $info['rss_import_showlink'] = "View the full article";
+ }
+
+ if ( !isset( $info['rss_import_topic_open'] ) )
+ {
+ $info['rss_import_topic_open'] = 1;
+ }
+
+ if ( !isset( $info['rss_import_topic_hide'] ) )
+ {
+ $info['rss_import_topic_hide'] = 0;
+ }
+
+ if ( !isset( $info['rss_import_topic_pre'] ) )
+ {
+ $info['rss_import_topic_pre'] = '';
+ }
+
+ if ( !isset( $info['rss_import_allow_html'] ) )
+ {
+ $info['rss_import_allow_html'] = 0;
+ }
+
+ if ( isset( $info['rss_import_auth'] ) AND isset( $info['rss_import_auth_user'] ) AND isset( $info['rss_import_auth_pass'] ) )
+ {
+ if ( $info['rss_import_auth'] == 0 )
+ {
+ $info['rss_import_auth_user'] = NULL;
+ $info['rss_import_auth_pass'] = NULL;
+ }
+ }
+ else
+ {
+ $info['rss_import_auth'] = 0;
+ $info['rss_import_auth_user'] = NULL;
+ $info['rss_import_auth_pass'] = NULL;
+ }
+
+ $id = $info['rss_import_id'];
+ unset( $info['rss_import_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_rss_import', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'forums_rss_import' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an RSS Imported Topic... or try to, anyway.
+ *
+ * @param array $info Data to insert
+ * @return boolean TRUE on success, or FALSE on failure.
+ */
+ public function convertForumsRssImported( $info=array() )
+ {
+ /* Godspeed */
+ if ( isset( $info['rss_imported_impid'] ) )
+ {
+ try
+ {
+ $info['rss_imported_impid'] = $this->software->app->getLink( $info['rss_imported_impid'], 'forums_rss_import' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rss_imported_missing_feed', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'rss_imported_missing_feed', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['rss_imported_tid'] ) )
+ {
+ try
+ {
+ $info['rss_imported_tid'] = $this->software->app->getLink( $info['rss_imported_tid'], 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'rss_imported_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'rss_imported_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rss_imported_guid'] ) )
+ {
+ $this->software->app->log( 'rss_imported_missing_guid', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ \IPS\Db::i()->insert( 'forums_rss_imported', $info );
+ return TRUE;
+ }
+
+ /**
+ * Convert a question rating
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted rating, or FALSE on failure.
+ */
+ public function convertForumsQuestionRating( $info=array() )
+ {
+ /* We do not need to store an ID if one isn't present */
+ $hasId = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ /* Everything but the date is required */
+ if ( isset( $info['topic'] ) )
+ {
+ try
+ {
+ $info['topic'] = $this->software->app->getLink( $info['topic'], 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'question_rating_missing_question', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'question_rating_missing_question', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['forum'] ) )
+ {
+ try
+ {
+ $info['forum'] = $this->software->app->getLink( $info['forum'], 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'question_rating_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'question_rating_missing_forum', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['member'] ) )
+ {
+ try
+ {
+ $info['member'] = $this->software->app->getLink( $info['member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'question_rating_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'question_rating_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rating'] ) )
+ {
+ $this->software->app->log( 'question_rating_missing_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ else
+ {
+ if ( $info['rating'] >= 1 )
+ {
+ $info['rating'] = 1;
+ }
+ else if ( $info['rating'] <= -1 ) # that always looks so weird
+ {
+ $info['rating'] = -1;
+ }
+ else
+ {
+ $this->software->app->log( 'question_rating_invalid_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+
+ if ( isset( $info['date'] ) )
+ {
+ if ( $info['date'] instanceof \IPS\DateTime )
+ {
+ $info['date'] = $info['date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['date'] = time();
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['id'];
+ unset( $info['id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_question_ratings', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'forums_question_ratings' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Answer Rating
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted rating, or FALSE on failure.
+ */
+ public function convertForumsAnswerRating( $info=array() )
+ {
+ /* We do not need to store an ID if one is not present */
+ $hasId = TRUE;
+ if ( !isset( $info['id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ /* Everything but the date is required */
+ if ( isset( $info['post'] ) )
+ {
+ try
+ {
+ $info['post'] = $this->software->app->getLink( $info['post'], 'forums_posts' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'answer_rating_missing_answer', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'answer_rating_missing_answer', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['member'] ) )
+ {
+ try
+ {
+ $info['member'] = $this->software->app->getLink( $info['member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'answer_rating_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'answer_rating_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( !isset( $info['rating'] ) )
+ {
+ $this->software->app->log( 'answer_rating_missing_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ else
+ {
+ if ( $info['rating'] >= 1 )
+ {
+ $info['rating'] = 1;
+ }
+ else if ( $info['rating'] <= -1 )
+ {
+ $info['rating'] = -1;
+ }
+ else
+ {
+ $this->software->app->log( 'answer_rating_invalid_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+
+ if ( isset( $info['date'] ) )
+ {
+ if ( $info['date'] instanceof \IPS\DateTime )
+ {
+ $info['date'] = $info['date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['date'] = time();
+ }
+
+ if ( isset( $info['topic'] ) )
+ {
+ try
+ {
+ $info['topic'] = $this->software->app->getLink( $info['topic'], 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'answer_rating_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'answer_rating_missing_topic', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['id'] : NULL );
+ return FALSE;
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['id'];
+ unset( $info['id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'forums_answer_ratings', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'forums_answer_ratings' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an Attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Array of Map data
+ * @param NULL|string $filepath The path to the attachment files or NULL if loading from the database.
+ * @param NULL|string $filedata If loading from the database, the content of the Binary column.
+ * @return boolean|integer The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ /* The Core Library handles most of this. We just need to make sure forum specific data is set. */
+ $map['id1_type'] = 'forums_topics';
+ $map['id1_from_parent'] = FALSE;
+ $map['id2_type'] = 'forums_posts';
+ $map['id2_from_parent'] = FALSE;
+ $map['location_key'] = 'forums_Forums';
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Gallery.php b/applications/convert/sources/Library/Gallery.php
new file mode 100644
index 00000000..941ea17f
--- /dev/null
+++ b/applications/convert/sources/Library/Gallery.php
@@ -0,0 +1,1181 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Gallery extends Core
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'gallery';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertGalleryCategories':
+ $return[ $k ] = array(
+ 'step_method' => 'convertGalleryCategories',
+ 'step_title' => 'convert_gallery_categories',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'gallery_categories' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'gallery_categories',
+ );
+ break;
+
+ case 'convertGalleryAlbums':
+ /* Some software do not have a category concept */
+ $dependencies = array();
+
+ if ( array_key_exists( 'convertGalleryCategories', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGalleryCategories';
+ }
+
+ $return[ $k ] = array(
+ 'step_method' => 'convertGalleryAlbums',
+ 'step_title' => 'convert_gallery_albums',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'gallery_albums' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'gallery_albums',
+ );
+ break;
+
+ case 'convertGalleryImages':
+ $dependencies = array();
+
+ /* If the source is really weird and has neither, we can make do with creating manually */
+ if ( array_key_exists( 'convertGalleryCategories', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGalleryCategories';
+ }
+
+ if ( array_key_exists( 'convertGalleryAlbums', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGalleryAlbums';
+ }
+
+ $return[ $k ] = array(
+ 'step_method' => 'convertGalleryImages',
+ 'step_title' => 'convert_gallery_images',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'gallery_images' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'gallery_images',
+ );
+ break;
+
+ case 'convertGalleryComments':
+ $return[ $k ] = array(
+ 'step_method' => 'convertGalleryComments',
+ 'step_title' => 'convert_gallery_comments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'gallery_comments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertGalleryImages' ),
+ 'link_type' => 'gallery_comments',
+ );
+ break;
+
+ case 'convertGalleryReviews':
+ $return[ $k ] = array(
+ 'step_method' => 'convertGalleryReviews',
+ 'step_title' => 'convert_gallery_reviews',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'gallery_reviews' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertGalleryImages' ),
+ 'link_type' => 'gallery_reviews',
+ );
+ break;
+
+ case 'convertAttachments':
+ $dependencies = array( 'convertGalleryImages' );
+
+ if ( array_key_exists( 'convertGalleryComments', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGalleryComments';
+ }
+
+ if ( array_key_exists( 'convertGalleryReviews', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertGalleryReviews';
+ }
+
+ $return[ $k ] = array(
+ 'step_method' => 'convertAttachments',
+ 'step_title' => 'convert_attachments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'core_attachments_map', array( "location_key=?", 'gallery_Images' ) )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'core_attachments',
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertGalleryCategories':
+ $return['convertGalleryCategories'] = array( 'gallery_categories' => NULL );
+ break;
+
+ case 'convertGalleryAlbums':
+ $return['convertGalleryAlbums'] = array( 'gallery_albums' => NULL );
+ break;
+
+ case 'convertGalleryImages':
+ $return['convertGalleryImages'] = array( 'gallery_images' => NULL );
+ break;
+
+ case 'convertGalleryComments':
+ $return['convertGalleryComments'] = array( 'gallery_comments' => NULL );
+ break;
+
+ case 'convertGalleryReviews':
+ $return['convertGalleryReviews'] = array( 'gallery_reviews' => NULL );
+ break;
+
+ case 'convertAttachments':
+ $return['convertAttachments'] = array( 'core_attachments' => \IPS\Db::i()->select( 'attachment_id', 'core_attachments_map', array( "location_key=?", 'gallery_Gallery' ) ), 'core_attachments_map' => array( 'location_key=?', 'gallery_Gallery' ) );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a category
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly converted category, or FALSE on error.
+ */
+ public function convertGalleryCategory( $info=array() )
+ {
+ if ( !isset( $info['category_id'] ) )
+ {
+ $this->software->app->log( 'gallery_category_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( !isset( $info['category_name'] ) )
+ {
+ $name = "Untitled Category {$info['category_id']}";
+ $this->software->app->log( 'gallery_category_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['category_id'] );
+ }
+ else
+ {
+ $name = $info['category_name'];
+ unset( $info['category_name'] );
+ }
+
+ if ( !isset( $info['category_desc'] ) )
+ {
+ $desc = '';
+ }
+ else
+ {
+ $desc = $info['category_desc'];
+ unset( $info['category_desc'] );
+ }
+
+ if ( isset( $info['category_parent_id'] ) )
+ {
+ try
+ {
+ $info['category_parent_id'] = $this->software->app->getLink( $info['category_parent_id'], 'gallery_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['category_conv_parent'] = $info['category_parent_id'];
+ }
+ }
+ else
+ {
+ $info['category_parent_id'] = 0;
+ }
+
+ $info['category_name_seo'] = \IPS\Http\Url::seoTitle( $name );
+
+ /* Counts */
+ foreach( array( 'category_count_imgs', 'category_count_comments', 'category_count_imgs_hidden', 'category_count_comments_hidden', 'category_public_albums', 'category_nonpublic_albums', 'category_rating_aggregate', 'category_rating_count', 'category_rating_total' ) AS $count )
+ {
+ if ( !isset( $info[ $count ] ) )
+ {
+ $info[ $count ] = 0;
+ }
+ }
+
+ /* We don't know these yet, but we may be able to figure them out later. */
+ $info['category_cover_img_id'] = 0;
+ $info['category_last_img_id'] = 0;
+ $info['category_last_img_date'] = 0;
+
+ if ( !isset( $info['category_sort_options'] ) OR !in_array( $info['category_sort_options'], array( 'updated', 'rating', 'num_comments' ) ) )
+ {
+ $info['category_sort_options'] = NULL;
+ }
+
+ /* One Defaults */
+ foreach( array( 'category_allow_comments', 'category_allow_rating', 'category_allow_albums' ) AS $oneDefault )
+ {
+ if ( !isset( $info[ $oneDefault ] ) )
+ {
+ $info[ $oneDefault ] = 1;
+ }
+ }
+
+ /* Zero Defaults */
+ foreach( array( 'category_approve_img', 'category_approve_com', 'category_watermark', 'category_can_tag', 'category_tag_prefixes', 'category_show_rules', 'category_allow_reviews', 'category_review_moderate' ) AS $zeroDefault )
+ {
+ if ( !isset( $info[ $zeroDefault ] ) )
+ {
+ $info[ $zeroDefault ] = 0;
+ }
+ }
+
+ /* Not Used */
+ $info['category_preset_tags'] = NULL;
+ $info['category_rules_link'] = NULL;
+
+ if ( !isset( $info['category_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(category_position)', 'gallery_categories' )->first();
+
+ $info['category_position'] = $position + 1;
+ }
+
+ $id = $info['category_id'];
+ unset( $info['category_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'gallery_categories', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'gallery_categories' );
+
+ \IPS\Lang::saveCustom( 'gallery', "gallery_category_{$inserted_id}", $name );
+ \IPS\Lang::saveCustom( 'gallery', "gallery_category_{$inserted_id}_desc", $desc );
+
+ \IPS\Db::i()->update( 'gallery_categories', array( "category_parent_id" => $inserted_id ), array( "category_conv_parent=? AND category_conv_parent<>?", $id, 0 ) );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an album
+ *
+ * @param array $info Data to insert
+ * @param array|NULL $socialgroup Array of social group data, or NULL.
+ * @param integer|NULL $category If the source software only has albums, and not categories, an existing category ID can be passed here to store converted albums. NULL to auto-create.
+ * @return integer|boolean The ID of the newly converted album, or FALSE on error.
+ */
+ public function convertGalleryAlbum( $info=array(), $socialgroup=NULL, $category=NULL )
+ {
+ if ( !isset( $info['album_id'] ) )
+ {
+ $this->software->app->log( 'gallery_album_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( is_null( $category ) )
+ {
+ if ( isset( $info['album_category_id'] ) )
+ {
+ try
+ {
+ $info['album_category_id'] = $this->software->app->getLink( $info['album_category_id'], 'gallery_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['album_category_id'] = $this->_orphanedAlbumsCategory();
+ }
+ }
+ else
+ {
+ $info['album_category_id'] = $this->_orphanedAlbumsCategory();
+ }
+ }
+ else
+ {
+ $info['album_category_id'] = $category;
+ }
+
+ if ( isset( $info['album_owner_id'] ) )
+ {
+ try
+ {
+ $info['album_owner_id'] = $this->software->app->getLink( $info['album_owner_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['album_owner_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['album_owner_id'] = 0;
+ }
+
+ if ( !isset( $info['album_name'] ) )
+ {
+ $info['album_name'] = "Untitled Album {$info['album_id']}";
+ $this->software->app->log( 'gallery_album_missing_name', __METHOD__, \IPS\convert\App::LOG_NOTICE, $info['album_id'] );
+ }
+
+ $info['album_name_seo'] = \IPS\Http\Url::seoTitle( $info['album_name'] );
+
+ if ( !isset( $info['album_description'] ) )
+ {
+ $info['album_description'] = '';
+ }
+
+ if ( !isset( $info['album_type'] ) OR !in_array( $info['album_type'], array( 1, 2, 3 ) ) )
+ {
+ $info['album_type'] = 1;
+ }
+
+ /* Counts */
+ foreach( array( 'album_count_imgs', 'album_count_comments', 'album_count_imgs_hidden', 'album_rating_aggregate', 'album_rating_count', 'album_rating_total', 'album_count_reviews', 'album_count_reviews_hidden' ) AS $count )
+ {
+ if ( !isset( $info[ $count ] ) )
+ {
+ $info[ $count ] = 0;
+ }
+ }
+
+ /* These are things we will not know */
+ $info['album_cover_img_id'] = 0;
+ $info['album_last_img_id'] = 0;
+ $info['album_last_img_date'] = 0;
+ $info['album_last_x_images'] = NULL;
+
+ if ( !isset( $info['album_sort_options'] ) OR !in_array( $info['album_sort_options'], array( 'updated', 'last_comment', 'title', 'rating', 'date', 'num_comments', 'num_reviews', 'views' ) ) )
+ {
+ $info['album_sort_options'] = 'updated';
+ }
+
+ if ( !isset( $info['album_allow_comments'] ) )
+ {
+ $info['album_allow_comments'] = 1;
+ }
+
+ if ( !isset( $info['album_allow_rating'] ) )
+ {
+ $info['album_allow_rating'] = 1;
+ }
+
+ if ( !isset( $info['album_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(album_position)', 'gallery_albums', array( "album_owner_id=?", $info['album_owner_id'] ) )->first();
+
+ $info['album_position'] = $position + 1;
+ }
+
+ $info['album_allowed_access'] = NULL;
+ if ( !is_null( $socialgroup ) AND is_array( $socialgroup ) )
+ {
+ $socialGroupId = \IPS\Db::i()->insert( 'core_sys_social_groups', array( 'owner_id' => $info['album_owner_id'] ) );
+ $members = array();
+ $members[] = array( 'group_id' => $socialGroupId, 'member_id' => $info['album_owner_id'] );
+ foreach( $socialgroup['members'] AS $member )
+ {
+ try
+ {
+ $members[] = array( 'group_id' => $socialGroupId, 'member_id' => $this->software->app->getLink( $member, 'core_members', TRUE ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+ \IPS\Db::i()->insert( 'core_sys_social_group_members', $members );
+
+ $info['album_allowed_access'] = $socialGroupId;
+ }
+
+ if ( !isset( $info['album_allow_reviews'] ) )
+ {
+ $info['album_allow_reviews'] = 1;
+ }
+
+ $id = $info['album_id'];
+ unset( $info['album_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'gallery_albums', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'gallery_albums' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Orphaned Albums Category
+ *
+ * @return integer Orphaned Albums Category ID
+ */
+ protected function _orphanedAlbumsCategory()
+ {
+ try
+ {
+ return $this->software->app->getLink( '__orphaned__', 'gallery_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ return $this->convertGalleryCategory( array(
+ 'category_id' => '__orphaned__',
+ 'category_name' => 'Converted Albums',
+ ) );
+ }
+ }
+
+ /**
+ * Convert an image
+ *
+ * @param array $info Data to insert
+ * @param string|NULL $filepath The path to the image, or NULL.
+ * @param string|NULL $fileinfo The binary data for the image, or NULL
+ * @return integer|boolean The ID of the newly inserted image, or FALSE on failure.
+ */
+ public function convertGalleryImage( $info=array(), $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['image_id'] ) )
+ {
+ $this->software->app->log( 'gallery_image_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* If we don't have any image to process, just stop here */
+ if ( is_null( $filedata ) AND ( is_null( $filepath ) OR !file_exists( $filepath ) ) )
+ {
+ $this->software->app->log( 'gallery_image_missing_image', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['image_id'] );
+ return FALSE;
+ }
+
+ /* Do this first - we may not have a category so we can figure it out here */
+ $categoryFound = FALSE;
+ if ( isset( $info['image_album_id'] ) )
+ {
+ /* We only want to do this if the Album ID is explicitly not 0, as that means it has no album, which is okay */
+ if ( $info['image_album_id'] > 0 )
+ {
+ try
+ {
+ $info['image_album_id'] = $this->software->app->getLink( $info['image_album_id'], 'gallery_albums' );
+
+ $album = \IPS\gallery\Album::load( $info['image_album_id'] );
+ $info['image_category_id'] = $album->category_id;
+ $categoryFound = TRUE;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* We can just dump directly into the category */
+ $info['image_album_id'] = 0;
+ }
+ }
+ }
+ else
+ {
+ $info['image_album_id'] = 0;
+ }
+
+ if ( isset( $info['image_category_id'] ) AND $categoryFound == FALSE )
+ {
+ try
+ {
+ $info['image_category_id'] = $this->software->app->getLink( $info['image_category_id'], 'gallery_categories' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['image_category_id'] = $this->_orphanedAlbumsCategory();
+ }
+ }
+ else
+ {
+ if ( $categoryFound == FALSE )
+ {
+ $info['image_category_id'] = $this->_orphanedAlbumsCategory();
+ }
+ }
+
+ if ( isset( $info['image_member_id'] ) )
+ {
+ try
+ {
+ $info['image_member_id'] = $this->software->app->getLink( $info['image_member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Set it to NULL - we can try a few other methods. */
+ $info['image_member_id'] = NULL;
+ }
+ }
+ else
+ {
+ $info['image_member_id'] = NULL;
+ }
+
+ if ( is_null( $info['image_member_id'] ) )
+ {
+ try
+ {
+ $album = \IPS\gallery\Album::load( $info['image_album_id'] );
+ $info['image_member_id'] = (int) $album->owner()->member_id;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['image_member_id'] = 0;
+ }
+ }
+
+ if ( !isset( $info['image_caption'] ) )
+ {
+ if ( !is_null( $filepath ) )
+ {
+ $caption = explode( '/', $info['image_caption'] );
+ $caption = array_pop( $caption );
+ }
+ else
+ {
+ $caption = "Untitled Image {$info['image_id']}";
+ }
+
+ $info['image_caption'] = $caption;
+ }
+
+ if ( mb_strlen( $info['image_caption'] ) > 255 )
+ {
+ $info['image_caption'] = mb_substr( $info['image_caption'], 0, 200 );
+ }
+
+ if ( !isset( $info['image_description'] ) )
+ {
+ $info['image_description'] = NULL;
+ }
+
+ /* We will build these later */
+ $info['image_masked_file_name'] = NULL;
+ $info['image_medium_file_name'] = NULL;
+ $info['image_small_file_name'] = NULL;
+ $info['image_thumb_file_name'] = NULL;
+
+ /* Figure this out before we bother creating the image */
+ if ( !isset( $info['image_file_type'] ) )
+ {
+ $mime_type = \IPS\File::getMimeType( $info['image_file_name'] );
+
+ if ( $mime_type === 'application/x-unknown' OR ( \strstr( $mime_type, 'image' ) === FALSE AND \strstr( $mime_type, 'video' ) === FALSE ) )
+ {
+ $this->software->app->log( 'gallery_image_invalid_mime_type', __METHOD__, \IPS\convert\App::LOG_WARNING, $image['image_id'] );
+ return FALSE;
+ }
+
+ $info['image_file_type'] = $mime_type;
+ }
+
+ if ( !isset( $info['image_file_name'] ) )
+ {
+ /* If we were passed a path, try and figure it out */
+ if ( !is_null( $filepath ) )
+ {
+ $file_name = explode( '/', $filepath );
+ $file_name = array_pop( $file_name );
+ $info['image_file_name'] = $file_name;
+ }
+ else
+ {
+ /* We can't do much here... we have binary data, but no way to figure out the file name */
+ $this->software->app->log( 'gallery_image_missing_file_name', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['image_id'] );
+ return FALSE;
+ }
+ }
+
+ /* Okay, let's create our image. We can use data returned from this later if it's missing. */
+ try
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = file_get_contents( $filepath );
+ }
+ $image = \IPS\File::create( 'gallery_Images', $info['image_file_name'], $filedata );
+ $info['image_original_file_name'] = (string) $image;
+
+ if ( !isset( $info['image_file_size'] ) )
+ {
+ $info['image_file_size'] = $image->filesize();
+ }
+ }
+ catch( \Exception $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['image_id'] );
+ return FALSE;
+ }
+ catch( \ErrorException $e )
+ {
+ $this->software->app->log( $e->getMessage(), __METHOD__, \IPS\convert\App::LOG_WARNING, $info['image_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['image_approved'] ) )
+ {
+ $info['image_approved'] = 1;
+ }
+
+ /* Counts */
+ foreach( array( 'image_views', 'image_comments', 'image_comments_queued', 'image_ratings_total', 'image_ratings_count', 'image_rating', 'image_hidden_comments', 'image_unapproved_comments', 'image_reviews', 'image_hidden_reviews', 'image_unapproved_reviews' ) AS $count )
+ {
+ if ( !isset( $info[ $count ] ) )
+ {
+ $info[ $count ] = 0;
+ }
+ }
+
+ /* Dates */
+ foreach( array( 'image_date', 'image_last_comment', 'image_updated' ) AS $date )
+ {
+ if ( isset( $info[ $date ] ) )
+ {
+ if ( $info[ $date ] instanceof \IPS\DateTime )
+ {
+ $info[ $date ] = $info[ $date ]->getTimestamp();
+ }
+ }
+ else
+ {
+ $info[ $date ] = time();
+ }
+ }
+
+ if ( !isset( $info['image_pinned'] ) )
+ {
+ $info['image_pinned'] = 0;
+ }
+
+ if ( !isset( $info['image_media'] ) )
+ {
+ if ( \strstr( $info['image_file_type'], 'video' ) !== FALSE )
+ {
+ $info['image_media'] = 1;
+ }
+ else
+ {
+ $info['image_media'] = 0;
+ }
+ }
+
+ if ( !isset( $info['image_credit_info'] ) )
+ {
+ $info['image_credit_info'] = NULL;
+ }
+
+ if ( !isset( $info['image_copyright'] ) )
+ {
+ $info['image_copyright'] = NULL;
+ }
+
+ if ( !isset( $info['image_metadata'] ) )
+ {
+ try
+ {
+ $exif = \IPS\Image::create( $image->contents() )->getExifData();
+ }
+ catch( \LogicException $e )
+ {
+ $exif = NULL;
+ }
+
+ $info['image_metadata'] = json_encode( $exif );
+ }
+
+ $info['image_caption_seo'] = \IPS\Http\Url::seoTitle( $info['image_caption'] );
+
+ if ( !isset( $info['image_privacy'] ) )
+ {
+ $info['image_privacy'] = 0;
+ }
+
+ $info['image_parent_permission'] = NULL;
+
+ if ( !isset( $info['image_feature_flag'] ) )
+ {
+ $info['image_feature_flag'] = 0;
+ }
+
+ /* GeoLocation Stuffs */
+ if ( isset( $info['image_geolocation'] ) AND $info['image_geolocation'] instanceof \IPS\GeoLocation )
+ {
+ if ( !isset( $info['image_gps_show'] ) )
+ {
+ $info['image_gps_show'] = 0;
+ }
+ $info['image_geolocation']->getLatLong();
+ $info['image_gps_lat'] = $info['image_geolocation']->lat;
+ $info['image_gps_lon'] = $info['image_geolocation']->long;
+ $info['image_loc_short'] = (string) $info['image_geolocation'];
+ $info['image_gps_raw'] = json_encode( $info['image_geolocation'] );
+ unset( $info['image_geolocation'] );
+ }
+ else
+ {
+ $info['image_gps_show'] = 0;
+ $info['image_gps_lat'] = NULL;
+ $info['image_gps_lon'] = NULL;
+ $info['image_loc_short'] = NULL;
+ $info['image_gps_raw'] = NULL;
+ }
+
+ if ( isset( $info['image_approved_by'] ) )
+ {
+ try
+ {
+ $info['image_approved_by'] = $this->software->app->getLink( $info['image_approved_by'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['image_approved_by'] = NULL;
+ }
+ }
+ else
+ {
+ $info['image_approved_by'] = NULL;
+ }
+
+ if ( isset( $info['image_approved_on'] ) )
+ {
+ if ( $info['image_approved_on'] instanceof \IPS\DateTime )
+ {
+ $info['image_approved_on'] = $info['image_approved_on']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['image_approved_on'] = NULL;
+ }
+
+ if ( !isset( $info['image_locked'] ) )
+ {
+ $info['image_locked'] = 0;
+ }
+
+ if ( !isset( $info['image_ipaddress'] ) OR filter_var( $info['image_ipaddress'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['image_ipaddress'] = '127.0.0.1';
+ }
+
+ /* We'll build this later */
+ $info['image_data'] = NULL;
+
+ /* Whew */
+ $id = $info['image_id'];
+ unset( $info['image_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'gallery_images', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'gallery_images' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a comment
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted comment, or FALSE on failure.
+ */
+ public function convertGalleryComment( $info=array() )
+ {
+ if ( !isset( $info['comment_id'] ) )
+ {
+ $this->software->app->log( 'gallery_comment_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( empty( $info['comment_text'] ) )
+ {
+ $this->software->app->log( 'gallery_comment_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_img_id'] ) )
+ {
+ try
+ {
+ $info['comment_img_id'] = $this->software->app->getLink( $info['comment_img_id'], 'gallery_images' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'gallery_comment_missing_image', __METHOD__, \IPS\convert\ApP::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'gallery_comment_missing_image', __METHOD__, \IPS\convert\ApP::LOG_WARNING, $info['comment_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['comment_edit_time'] ) )
+ {
+ if ( $info['comment_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['comment_edit_time'] = $info['comment_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_edit_time'] = 0;
+ }
+
+ if ( isset( $info['comment_author_id'] ) )
+ {
+ try
+ {
+ $info['comment_author_id'] = $this->software->app->getLink( $info['comment_author_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['comment_author_id'] = 0;
+ }
+ }
+ else
+ {
+ $info['comment_author_id'] = 0;
+ }
+
+ if ( !isset( $info['comment_author_name'] ) )
+ {
+ $author = \IPS\Member::load( $info['comment_author_id'] );
+
+ if ( $author->member_id )
+ {
+ $info['comment_author_name'] = $author->name;
+ }
+ else
+ {
+ $info['comment_author_name'] = "Guest";
+ }
+ }
+
+ if ( !isset( $info['comment_ip_address'] ) OR filter_var( $info['comment_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['comment_ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['comment_post_date'] ) )
+ {
+ if ( $info['comment_post_date'] instanceof \IPS\DateTime )
+ {
+ $info['comment_post_date'] = $info['comment_post_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['comment_post_date'] = time();
+ }
+
+ if ( !isset( $info['comment_approved'] ) )
+ {
+ $info['comment_approved'] = 1;
+ }
+
+ if ( !isset( $info['comment_append_edit'] ) )
+ {
+ $info['comment_append_edit'] = 0;
+ }
+
+ if ( !isset( $info['comment_edit_name'] ) )
+ {
+ $info['comment_edit_name'] = NULL;
+ }
+
+ $id = $info['comment_id'];
+ unset( $info['comment_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'gallery_comments', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'gallery_comments' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a review
+ *
+ * @param array $info Data to insert
+ * @return integer|boolean The ID of the newly inserted reiew, or FALSE on failure.
+ */
+ public function convertGalleryReview( $info=array() )
+ {
+ if ( !isset( $info['review_id'] ) )
+ {
+ $this->software->app->log( 'gallery_review_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( empty( $info['review_content'] ) )
+ {
+ $this->software->app->log( 'gallery_review_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( !isset( $info['review_rating'] ) OR $info['review_rating'] < 1 )
+ {
+ $this->software->app->log( 'gallery_review_missing_rating', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_author'] ) )
+ {
+ try
+ {
+ $info['review_author'] = $this->software->app->getLink( $info['review_author'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'gallery_review_missing_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'gallery_review_missing_author', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+
+ if ( isset( $info['review_image_id'] ) )
+ {
+ try
+ {
+ $info['review_image_id'] = $this->software->app->getLink( $info['review_image_id'], 'gallery_images' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'gallery_review_missing_image', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['review_id'] );
+ return FALSE;
+ }
+ }
+
+ if ( !isset( $info['review_author_name'] ) )
+ {
+ $info['review_author_name'] = \IPS\Member::load( $info['review_author'] )->name;
+ }
+
+ if ( isset( $info['review_date'] ) )
+ {
+ if ( $info['review_date'] instanceof \IPS\DateTime )
+ {
+ $info['review_date'] = $info['review_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_date'] = time();
+ }
+
+ if ( !isset( $info['review_ip_address'] ) OR filter_var( $info['review_ip_address'], FILTER_VALIDATE_IP ) === FALSE )
+ {
+ $info['review_ip_address'] = '127.0.0.1';
+ }
+
+ if ( isset( $info['review_edit_time'] ) )
+ {
+ if ( $info['review_edit_time'] instanceof \IPS\DateTime )
+ {
+ $info['review_edit_time'] = $info['review_edit_time']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['review_edit_time'] = 0;
+ }
+
+ if ( !isset( $info['review_edit_member_name'] ) )
+ {
+ $info['review_edit_member_name'] = '';
+ }
+
+ if ( !isset( $info['review_edit_show'] ) )
+ {
+ $info['review_edit_show'] = 0;
+ }
+
+ if ( isset( $info['review_votes_data'] ) )
+ {
+ if ( !is_array( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_data'] = json_decode( $info['review_votes_data'], TRUE );
+ }
+
+ $newVoters = array();
+ if ( !is_null( $info['review_votes_data'] ) AND count( $info['review_votes_data'] ) )
+ {
+ foreach( $info['review_votes_data'] AS $member => $vote )
+ {
+ try
+ {
+ $memberId = $this->software->app->getLink( $member, 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+
+ $newVoters[$memberId] = $vote;
+ }
+ }
+
+ if ( count( $newVoters ) )
+ {
+ $info['review_votes_data'] = json_encode( $newVoters );
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+ }
+ else
+ {
+ $info['review_votes_data'] = NULL;
+ }
+
+ if ( !isset( $info['review_votes'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes'] = 0;
+ }
+ else
+ {
+ $info['review_votes'] = count( json_decode( $info['review_votes_data'], TRUE ) );
+ }
+ }
+
+ if ( !isset( $info['review_votes_helpful'] ) )
+ {
+ if ( is_null( $info['review_votes_data'] ) )
+ {
+ $info['review_votes_helpful'] = 0;
+ }
+ else
+ {
+ $helpful = 0;
+ foreach( json_decode( $info['review_votes_data'], TRUE ) AS $member => $vote )
+ {
+ if ( $vote == 1 )
+ {
+ $helpful += 1;
+ }
+ }
+
+ $info['review_votes_helpful'] = $helpful;
+ }
+ }
+
+ if ( !isset( $info['review_approved'] ) )
+ {
+ $info['review_approved'] = 1;
+ }
+
+ $id = $info['review_id'];
+ unset( $info['review_id'] );
+
+ $inserted_id = \IPS\Db::i()->insert( 'gallery_reviews', $info );
+ $this->software->app->addLink( $inserted_id, $id, 'gallery_reviews' );
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert an attachment
+ *
+ * @param array $info Data to insert
+ * @param array $map Attachment Map Data
+ * @param string|NULL $filepath The path to the attachment, or NULL.
+ * @param string|NULL $filedata The binary data of the attachment, or NULL.
+ * @return integer|boolean The ID of the newly inserted attachment, or FALSE on failure.
+ */
+ public function convertAttachment( $info=array(), $map=array(), $filepath=NULL, $filedata=NULL )
+ {
+ $map['location_key'] = 'gallery_Gallery';
+ $map['id1_type'] = 'gallery_images';
+ $map['id1_from_parent'] = FALSE;
+ $map['id2_from_parent'] = FALSE;
+ /* Some set up */
+ if ( !isset( $info['id3'] ) )
+ {
+ $info['id3'] = NULL;
+ }
+
+ if ( is_null( $info['id3'] ) OR $info['id3'] != 'review' )
+ {
+ $map['id2_type'] = 'gallery_comments';
+ }
+ else
+ {
+ $map['id2_type'] = 'gallery_reviews';
+ }
+
+ return parent::convertAttachment( $info, $map, $filepath, $filedata );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Library/Nexus.php b/applications/convert/sources/Library/Nexus.php
new file mode 100644
index 00000000..628bf745
--- /dev/null
+++ b/applications/convert/sources/Library/Nexus.php
@@ -0,0 +1,1343 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 6 May 2016
+ */
+
+namespace IPS\convert\Library;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Nexus extends \IPS\convert\Library
+{
+ /**
+ * @brief Application
+ */
+ public $app = 'nexus';
+
+ /**
+ * Returns an array of items that we can convert, including the amount of rows stored in the Community Suite as well as the recommend value of rows to convert per cycle
+ *
+ * @return array
+ */
+ public function menuRows()
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ foreach( $this->getConvertableItems() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertNexusCustomerFields':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_customer_fields',
+ 'step_method' => 'convertNexusCustomerFields',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_fields' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'nexus_customer_fields'
+ );
+ break;
+
+ case 'convertNexusCustomers':
+ $dependencies = array();
+ if ( in_array( 'convertNexusCustomerFields', $this->getConvertableItems() ) )
+ {
+ $dependencies[] = 'convertNexusCustomerFields';
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_customers',
+ 'step_method' => 'convertNexusCustomers',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_customers' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'nexus_customers',
+ );
+ break;
+
+ case 'convertNexusCustomerAddresses':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_customer_addresses',
+ 'step_method' => 'convertNexusCustomerAddresses',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_addresses' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertNexusCustomers' ),
+ 'link_type' => 'nexus_customer_addresses',
+ );
+ break;
+
+ case 'convertNexusCustomerHistory':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_customer_history',
+ 'step_method' => 'convertNexusCustomerHistory',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_customer_history' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertNexusCustomers' ),
+ 'link_type' => 'nexus_customer_history',
+ );
+ break;
+
+ case 'convertNexusAlternateContacts':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_alternate_contacts',
+ 'step_method' => 'convertNexusAlternateContacts',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_alternate_contacts' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertNexusCustomers' ),
+ 'link_type' => 'nexus_alternate_contacts'
+ );
+ break;
+
+ case 'convertNexusNotes':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_notes',
+ 'step_method' => 'convertNexusNotes',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_notes' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertNexusCustomers' ),
+ 'linK_type' => 'nexus_notes'
+ );
+ break;
+
+ case 'convertNexusPackageGroups':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_package_groups',
+ 'step_method' => 'convertNexusPackageGroups',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_package_groups' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'nexus_package_groups'
+ );
+ break;
+
+ case 'convertNexusPackageFields':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_package_fields',
+ 'step_method' => 'convertNexusPackageFields',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_package_fields' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'nexus_package_fields'
+ );
+ break;
+
+ case 'convertNexusPackages':
+ $dependencies = array();
+ foreach( array( 'convertNexusPackageGroups', 'convertNexusPackageFields' ) as $dependency )
+ {
+ $canConvert = $this->getConvertableItems();
+ if ( isset( $canConvert[ $dependency ] ) )
+ {
+ $dependencies[] = $dependency;
+ }
+ }
+
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_packages',
+ 'step_method' => 'convertNexusPackages',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_packages' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => $dependencies,
+ 'link_type' => 'nexus_packages',
+ );
+ break;
+
+ case 'convertNexusReviews':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_reviews',
+ 'step_method' => 'convertNexusReviews',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_reviews' )->first(),
+ 'source_rows' => $this->software->sourceRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertNexusPackages' ),
+ 'link_type' => 'nexus_reviews',
+ );
+ break;
+
+ case 'convertNexusCoupons':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_coupons',
+ 'step_method' => 'convertNexusCoupons',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_coupons' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertNexusPackages' ),
+ 'link_type' => 'nexus_coupons'
+ );
+ break;
+
+ case 'convertNexusInvoices':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_invoices',
+ 'step_method' => 'convertNexusInvoices',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_invoices' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertNexusCustomers', 'convertNexusPackages' ),
+ 'link_type' => 'nexus_invoices',
+ );
+ break;
+
+ case 'convertNexusTransactions':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_transactions',
+ 'step_method' => 'convertNexusTransactions',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_transactions' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertNexusCustomers', 'convertNexusInvoices' ),
+ 'link_type' => 'nexus_transactions',
+ );
+ break;
+
+ case 'convertNexusPurchases':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_purchases',
+ 'step_method' => 'convertNexusPurchases',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_purchases' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1000,
+ 'dependencies' => array( 'convertNexusCustomers', 'convertNexusPackages', 'convertNexusInvoices' ),
+ 'link_type' => 'nexus_purchases'
+ );
+ break;
+
+ case 'convertNexusFraudRules':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_fraud_rules',
+ 'step_method' => 'convertNexusFraudRules',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_fraud_rules' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array(),
+ 'link_type' => 'nexus_fraud_rules',
+ );
+ break;
+
+ case 'convertNexusSupportDepartments':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_departments',
+ 'step_method' => 'convertNexusSupportDepartments',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_departments' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertNexusPackages' ),
+ );
+ break;
+
+ case 'convertNexusSupportFields':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_fields',
+ 'step_method' => 'convertNexusSupportFields',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_fields' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertNexusSupportDepartments' ),
+ 'link_type' => 'nexus_support_fields',
+ );
+ break;
+
+ case 'convertNexusSupportStatuses':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_statuses',
+ 'step_method' => 'convertNexusSupportStatuses',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_statuses' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array(),
+ 'link_type' => 'nexus_support_statuses',
+ );
+ break;
+
+ case 'convertNexusSupportSeverities':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_severities',
+ 'step_method' => 'convertNexusSupportSeverities',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_severities' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertNexusSupportDepartments' ),
+ 'link_type' => 'nexus_support_severities'
+ );
+ break;
+
+ case 'convertNexusSupportStockActions':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_stock_actions',
+ 'step_method' => 'convertNexusSupportStockActions',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_stock_actions' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 100,
+ 'dependencies' => array( 'convertNexusSupportDepartments' ),
+ 'link_type' => 'nexus_support_stock_actions'
+ );
+ break;
+
+ case 'convertNexusSupportRequests':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_requests',
+ 'step_method' => 'convertNexusSupportRequests',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_requests' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 1500,
+ 'dependencies' => array( 'convertNexusCustomers', 'convertNexusSupportDepartments', 'convertNexusSupportFields' ),
+ 'link_type' => 'nexus_support_requests',
+ );
+ break;
+
+ case 'convertNexusSupportReplies':
+ $return[ $k ] = array(
+ 'step_title' => 'convert_nexus_support_replies',
+ 'step_method' => 'convertNexusSupportReplies',
+ 'ips_rows' => \IPS\Db::i()->select( 'COUNT(*)', 'nexus_support_replies' )->first(),
+ 'source_rows' => $this->software->countRows( $v['table'], $v['where'] ),
+ 'per_cycle' => 2000,
+ 'dependencies' => array( 'convertNexusSupportRequests' ),
+ 'link_type' => 'nexus_support_replies'
+ );
+ break;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Returns an array of tables that need to be truncated when Empty Local Data is used
+ *
+ * @param string The method to truncate.
+ * @return array
+ */
+ protected function truncate( $method )
+ {
+ $return = array();
+ $classname = get_class( $this->software );
+
+ if( $classname::canConvert() === NULL )
+ {
+ return array();
+ }
+
+ foreach( $classname::canConvert() as $k => $v )
+ {
+ switch( $k )
+ {
+ case 'convertNexusCustomerFields':
+ $return['convertNexusCustomerFields'] = array( 'nexus_customer_fields' => NULL );
+ break;
+
+ case 'convertNexusCustomers':
+ $return['convertNexusCustomers'] = array( 'nexus_customers' => array( "member_id<>?", \IPS\Member::loggedIn()->member_id ) );
+ break;
+
+ case 'convertNexusCustomerAddresses':
+ $return['convertNexusCustomerAddresses'] = array( 'nexus_customer_addresses' => NULL );
+ break;
+
+ case 'convertNexusCustomerHistory':
+ $return['convertNexusCustomerHistory'] = array( 'nexus_customer_history' => NULL );
+ break;
+
+ case 'convertNexusAlternateContacts':
+ $return['convertNexusAlternateContacts'] = array( 'nexus_alternate_contacts' => NULL );
+ break;
+
+ case 'convertNexusNotes':
+ $return['convertNexusNotes'] = array( 'nexus_notes' => NULL );
+ break;
+
+ case 'convertNexusPackageGroups':
+ $return['convertNexusPackageGroups'] = array( 'nexus_package_groups' => NULL );
+ break;
+
+ case 'convertNexusPackageFields':
+ $return['convertNexusPackageFields'] = array( 'nexus_package_fields' => NULL );
+ break;
+
+ case 'convertNexusPackages':
+ $return['convertNexusPackages'] = array(
+ 'nexus_packages' => NULL,
+ 'nexus_package_images' => NULL,
+ 'nexus_package_base_prices' => NULL,
+ 'nexus_packages_ads' => NULL,
+ 'nexus_packages_hosting' => NULL,
+ 'nexus_packages_products' => NULL,
+ );
+ break;
+
+ case 'convertNexusReviews':
+ $return['convertNexusReviews'] = array( 'nexus_reviews' => NULL );
+ break;
+
+ case 'convertNexusCoupons':
+ $return['convertNexusCoupons'] = array( 'nexus_coupons' => NULL );
+ break;
+
+ case 'convertNexusInvoices':
+ $return['convertNexusInvoices'] = array( 'nexus_invoices' => NULL );
+ break;
+
+ case 'convertNexusTransactions':
+ $return['convertNexusTransactions'] = array( 'nexus_transactions' => NULL );
+ break;
+
+ case 'convertNexusPurchases':
+ $return['convertNexusPurchases'] = array( 'nexus_purchases' => NULL );
+ break;
+
+ case 'convertNexusFraudRules':
+ $return['convertNexusFraudRules'] = array( 'nexus_fraud_rules' => NULL );
+ break;
+
+ case 'convertNexusSupportDepartments':
+ $return['convertNexusSupportDepartments'] = array( 'nexus_support_departments' => NULL );
+ break;
+
+ case 'convertNexusSupportFields':
+ $return['convertNexusSupportFields'] = array( 'nexus_support_fields' => NULL );
+ break;
+
+ case 'convertNexusSupportStatuses':
+ $return['convertNexusSupportStatuses'] = array( 'nexus_support_statuses' => NULL );
+ break;
+
+ case 'convertNexusSupportSeverities':
+ $return['convertNexusSupportSeverities'] = array( 'nexus_support_severities' => NULL );
+ break;
+
+ case 'convertNexusSupportStockActions':
+ $return['convertNexusSupportStockActions'] = array( 'nexus_support_stock_actions' => NULL );
+ break;
+
+ case 'convertNexusSupportRequests':
+ $return['convertNexusSupportRequests'] = array( 'nexus_support_requests' => NULL );
+ break;
+
+ case 'convertNexusSupportReplies':
+ $return['convertNexusSupportReplies'] = array( 'nexus_support_replies' => NULL );
+ break;
+ }
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * This is how the insert methods will work - basically like 3.x, but we should be using the actual classes to insert the data unless there is a real world reason not too.
+ * Using the actual routines to insert data will help to avoid having to resynchronize and rebuild things later on, thus resulting in less conversion time being needed overall.
+ * Anything that parses content, for example, may need to simply insert directly then rebuild via a task over time, as HTML Purifier is slow when mass inserting content.
+ */
+
+ /**
+ * A note on logging -
+ * If the data is missing and it is unlikely that any source software would be able to provide this, we do not need to log anything and can use default data (for example, group_layout in convertLeaderGroups).
+ * If the data is missing and it is likely that a majority of the source software can provide this, we should log a NOTICE and use default data (for example, a_casesensitive in convertAcronyms).
+ * If the data is missing and it is required to convert the item, we should log a WARNING and return FALSE.
+ * If the conversion absolutely cannot proceed at all (filestorage locations not writable, for example), then we should log an ERROR and throw an \IPS\convert\Exception to completely halt the process and redirect to an error screen showing the last logged error.
+ */
+
+ /**
+ * Convert a customer
+ *
+ * @param array Data to insert
+ * @param array Custom Field Data
+ * @return int|bool The ID of the inserted customer, or FALSE on failure
+ */
+ public function convertNexusCustomer( $info, $fields=array() )
+ {
+ if ( !isset( $info['member_id'] ) )
+ {
+ $this->software->app->log( 'nexus_customer_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+ else
+ {
+ $sourceId = $info['member_id'];
+
+ try
+ {
+ $info['member_id'] = $this->software->app->getLink( $info['member_id'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( 'nexus_customer_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['member_id'] );
+ return FALSE;
+ }
+ }
+
+ if ( !isset( $info['cm_first_name'] ) )
+ {
+ $info['cm_first_name'] = '';
+ }
+
+ if ( !isset( $info['cm_last_name'] ) )
+ {
+ $info['cm_last_name'] = '';
+ }
+
+ if ( !isset( $info['cm_phone'] ) )
+ {
+ $info['cm_phone'] = '';
+ }
+
+ if ( isset( $info['cm_profiles'] ) )
+ {
+ if ( is_array( $info['cm_profiles'] ) )
+ {
+ $info['cm_profiles'] = json_encode( $info['cm_profiles'] );
+ }
+ }
+ else
+ {
+ $info['cm_profiles'] = '';
+ }
+
+ $info = array_merge( $info, $this->_formatCustomerFields( $fields ) );
+
+ \IPS\Db::i()->replace( 'nexus_customers', $info );
+ $this->software->app->addLink( $info['member_id'], $sourceId, 'nexus_customers' );
+
+ return $info['member_id'];
+ }
+
+ /**
+ * Format Customer Fields for saving
+ *
+ * @param array The fields
+ * @return array Formatted Fields
+ */
+ protected function _formatCustomerFields( $fields )
+ {
+ $return = array();
+ foreach( $fields as $key => $value )
+ {
+ if ( is_array( $value ) )
+ {
+ $value = json_encode( $value );
+ }
+
+ try
+ {
+ $id = $this->software->app->getLink( str_replace( 'field_', '', $key ), 'nexus_customer_fields' );
+
+ $return['field_' . $id] = $value;
+ }
+ catch( \OutOfRangeException $e )
+ {
+ continue;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * Convert a note
+ *
+ * @param array Info
+ * @return int|bool The newly inserted note ID, or FALSE on failure
+ */
+ public function convertNexusNote( $info )
+ {
+ $hasId = TRUE;
+ if ( !isset( $info['note_id'] ) )
+ {
+ $hasId = FALSE;
+ }
+
+ if ( isset( $info['note_member'] ) )
+ {
+ try
+ {
+ $info['note_member'] = $this->software->app->getLink( $info['note_member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $this->software->app->log( "nexus_note_missing_member", __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['note_id'] : NULL );
+ return FALSE;
+ }
+ }
+ else
+ {
+ $this->software->app->log( 'nexus_note_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['note_id'] : NULL );
+ return FALSE;
+ }
+
+ if ( empty( $info['note_text'] ) )
+ {
+ $this->software->app->log( 'nexus_note_missing_content', __METHOD__, \IPS\convert\App::LOG_WARNING, ( $hasId ) ? $info['note_id'] : NULL );
+ return FALSE;
+ }
+
+ if ( isset( $info['note_author'] ) )
+ {
+ try
+ {
+ $info['note_author'] = $this->software->app->getLink( $info['note_author'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $info['note_author'] = 0;
+ }
+ }
+ else
+ {
+ $info['note_author'] = 0;
+ }
+
+ if ( isset( $info['note_date'] ) )
+ {
+ if ( $info['note_date'] instanceof \IPS\DateTime )
+ {
+ $info['note_date'] = $info['note_date']->getTimestamp();
+ }
+ }
+ else
+ {
+ $info['note_date'] = time();
+ }
+
+ if ( $hasId )
+ {
+ $id = $info['note_id'];
+ unset( $info['note_id'] );
+ }
+
+ $inserted_id = \IPS\Db::i()->insert( 'nexus_notes', $info );
+
+ if ( $hasId )
+ {
+ $this->software->app->addLink( $inserted_id, $id, 'nexus_notes' );
+ }
+
+ return $inserted_id;
+ }
+
+ /**
+ * Convert a package group
+ *
+ * @param array Info
+ * @param string|NULL Path to the groups cover photo, or NULL
+ * @param string|NULL Raw filedata for the groups cover photo, or NULL
+ * @return int|bool THe ID of the newly inserted package group, or FALSE on failure
+ */
+ public function convertNexusPackageGroup( $info, $filepath=NULL, $filedata=NULL )
+ {
+ if ( !isset( $info['pg_id'] ) )
+ {
+ $this->software->app->log( 'nexus_package_group_missing_ids', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['pg_name'] ) )
+ {
+ $name = $info['pg_name'];
+ unset( $info['pg_name'] );
+ }
+ else
+ {
+ $name = "Untitled Package Group {$info['pg_id']}";
+ }
+
+ if ( !isset( $info['pg_seo_name'] ) )
+ {
+ $info['pg_seo_name'] = \IPS\Http\Url::seoTitle( $name );
+ }
+
+ if ( !isset( $info['pg_position'] ) )
+ {
+ $position = \IPS\Db::i()->select( 'MAX(pg_position)', 'nexus_package_groups' )->first();
+
+ $info['pg_position'] = $position + 1;
+ }
+
+ if ( isset( $info['pg_parent'] ) )
+ {
+ $info['pg_conv_parent'] = $info['pg_parent'];
+ }
+ else
+ {
+ $info['pg_parent'] = 0;
+ }
+
+ if ( isset( $info['pg_image'] ) AND ( !is_null( $filepath ) OR !is_null( $filedata ) ) )
+ {
+ if ( is_null( $filedata ) AND !is_null( $filepath ) )
+ {
+ $filedata = @file_get_contents( rtrim( $filepath, '/' ) . '/' . $info['pg_image'] );
+ unset( $filepath );
+ }
+
+ if ( $filedata )
+ {
+ try
+ {
+ $file = \IPS\File::create( 'nexus_PackageGroups', $info['pg_image'], $filedata );
+ $info['pg_image'] = (string) $file;
+ }
+ catch( \Exception $e )
+ {
+ $info['pg_image'] = '';
+ }
+ }
+ else
+ {
+ $info['pg_image'] = '';
+ }
+ }
+ else
+ {
+ $info['pg_image'] = '';
+ }
+
+ $id = $info['pg_id'];
+ unset( $info['pg_id'] );
+
+ $insertedId = \IPS\Db::i()->insert( 'nexus_package_groups', $info );
+ $this->software->app->addLink( $insertedId, $id, 'nexus_package_groups' );
+
+ /* Custom Lang */
+ \IPS\Lang::saveCustom( 'nexus', "nexus_pgroup_{$insertedId}", $name );
+
+ \IPS\Db::i()->update( 'nexus_package_groups', array( 'pg_parent' => $insertedId ), array( "pg_conv_parent=?", $id ) );
+
+ return $insertedId;
+ }
+
+ /**
+ * Convert a package
+ *
+ * @param array $info Info
+ * @param string $filePath Path to images
+ * @return int|bool The ID of the newly inserted package, or FALSE on failure
+ */
+ public function convertNexusPackage( $info, $filePath=NULL )
+ {
+ /* !Required: ID */
+ if ( !isset( $info['p_id'] ) )
+ {
+ $this->software->app->log( 'nexus_package_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ if ( isset( $info['p_name'] ) )
+ {
+ $name = $info['p_name'];
+ unset( $info['p_name'] );
+ }
+ else
+ {
+ $name = "Untitled Package {$info['p_id']}";
+ }
+
+ if ( isset( $info['p_description'] ) )
+ {
+ $description = $info['p_description'];
+ unset( $info['p_description'] );
+ }
+ else
+ {
+ $description = '';
+ }
+
+ /* !Required: Price */
+ if( !isset( $info['p_base_price'] ) )
+ {
+ $this->software->app->log( 'nexus_package_missing_price', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['p_id'] );
+ return FALSE;
+ }
+
+ /* !Required: Package Group */
+ if( !isset( $info['p_group'] ) )
+ {
+ $this->software->app->log( 'nexus_package_missing_group', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['p_id'] );
+ return FALSE;
+ }
+ $info['p_group'] = $this->software->app->getLink( $info['p_group'], 'nexus_package_groups' );
+
+ if( isset( $info['p_support_severity'] ) )
+ {
+ $info['p_support_severity'] = $this->software->app->getLink( $info['p_support_severity'], 'nexus_support_severities' );
+ }
+
+ /* Viewable User Groups */
+ if( isset( $info['p_member_group'] ) AND $info['p_member_group'] != '*' )
+ {
+ $e = explode( ',', $info['p_member_group'] );
+ $newGroups = [];
+
+ array_walk( $info['p_member_group'], function( &$group, $key, &$newGroups )
+ {
+ try
+ {
+ $newGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $ex ) { }
+ }, $newGroups );
+
+ $info['p_member_group'] = $info['p_member_group'];
+ }
+
+ /* Primary User Group */
+ if( isset( $info['p_primary_group'] ) )
+ {
+ $info['p_primary_group'] = $this->software->app->getLink( $info['p_primary_group'], 'core_groups', TRUE );
+ }
+
+ /* Secondary User Groups */
+ if( isset( $info['p_secondary_group'] ) )
+ {
+ $e = explode( ',', $info['p_secondary_group'] );
+ $newSecondaryGroups = [];
+
+ array_walk( $info['p_secondary_group'], function( $group, $key, &$newSecondaryGroups )
+ {
+ try
+ {
+ $newSecondaryGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $ex ) { }
+ }, $newSecondaryGroups );
+ }
+
+ /* Associable Packages */
+ if( isset( $info['p_associable'] ) )
+ {
+ try
+ {
+ $info['p_associable'] = $this->software->app->getLink( $info['p_associable'], 'nexus_packages' );
+ }
+ catch( \OutOfRangeException $ex )
+ {
+ $info['conv_p_associable'] = $info['p_associable'];
+ unset( $info['p_associable'] );
+ }
+ }
+
+ if ( !isset( $info['p_position'] ) )
+ {
+ try
+ {
+ $position = \IPS\Db::i()->select( 'MAX(p_position)', 'nexus_packages' )->first();
+ $info['p_position'] = $position + 1;
+ }
+ catch( \UnderflowException $e ) { }
+ }
+
+ /* Default Currency */
+ $currency = \IPS\nexus\Customer::loggedIn()->defaultCurrency();
+ $price = new \IPS\nexus\Money( $info['p_base_price'], $currency );
+ $basePrice = [ $currency => [ 'amount' => $price->amount, 'currency' => $currency ] ];
+ unset( $info['p_base_price'] );
+
+ /* Renewal costs & terms */
+ if( isset( $info['p_renew_options'] ) AND count( $info['p_renew_options'] ) )
+ {
+ $newOptions = array();
+
+ foreach( $info['p_renew_options'] as $value )
+ {
+ $price = new \IPS\nexus\Money( $value['price'], $currency );
+ $renewPrice = [ $currency => [ 'amount' => $price->amount, 'currency' => $currency ] ];
+ $newOptions[] = array(
+ 'cost' => $renewPrice,
+ 'term' => $value['term'],
+ 'unit' => $value['unit'],
+ 'add' => isset( $value['add'] ) ? $value['add'] : false
+ );
+ }
+
+ $info['p_renew_options'] = json_encode( $newOptions );
+ }
+
+ /* Discounts */
+ //@TODO
+
+ $productImages = isset( $info['p_images'] ) ? $info['p_images'] : NULL;
+ unset( $info['p_images'] );
+
+ $package = array(
+ 'p_name' => $name,
+ 'p_seo_name' => \IPS\Http\url::seoTitle( $name ),
+ 'p_group' => 0,
+ 'p_stock' => -1,
+ 'p_reg' => 0,
+ 'p_store' => 1,
+ 'p_member_groups' => '*',
+ 'p_allow_upgrading' => 0,
+ 'p_upgrade_charge' => 0,
+ 'p_allow_downgrading' => 0,
+ 'p_downgrade_refund' => 0,
+ 'p_base_price' => json_encode( $basePrice ),
+ 'p_tax' => 0,
+ 'p_renewal_days' => 0,
+ 'p_primary_group' => 0,
+ 'p_secondary_group' => '',
+ 'p_return_primary' => 1,
+ 'p_return_secondary' => 0,
+ 'p_position' => 0,
+ 'p_associable' => '',
+ 'p_force_assoc' => 0,
+ 'p_assoc_error' => NULL,
+ 'p_discounts' => '[]',
+ 'p_page' => NULL,
+ 'p_support' => 0,
+ 'p_support_department' => 0,
+ 'p_support_severity' => 0,
+ 'p_featured' => 0,
+ 'p_upsell' => 0,
+ 'p_notify' => '',
+ 'p_type' => 'product',
+ 'p_custom' => 0,
+ 'p_reviewable' => 0,
+ 'p_review_moderate' => 0,
+ 'p_image' => NULL,
+ 'p_methods' => '*',
+ 'p_renew_options' => '',
+ 'p_group_renewals' => 0,
+ 'p_rebuild_thumb' => 0,
+ 'p_renewal_days_advance' => -1,
+ 'p_date_added' => time(),
+ 'p_reviews' => 0,
+ 'p_rating' => 0,
+ 'p_unapproved_reviews' => NULL,
+ 'p_hidden_reviews' => NULL,
+ 'p_grace_period' => 0,
+ 'p_conv_associable' => 0
+ );
+
+ /* Save Package Data */
+ $insertedId = \IPS\Db::i()->insert( 'nexus_packages', $this->_getValues( $package, $info ) );
+ $originalId = $info['p_id'];
+ unset( $info['p_id'] );
+
+ /* Images */
+ $images = NULL;
+
+ if( $productImages )
+ {
+ $default = 0;
+ foreach( $productImages as $image )
+ {
+ if ( ( isset( $image['data'] ) AND is_null( $image['data'] ) ) AND !is_null( $filePath ) )
+ {
+ $filedata = @file_get_contents( rtrim( $filePath, '/' ) . '/' . $image['filename'] );
+ unset( $filepath );
+ }
+
+ if ( $filedata )
+ {
+ try
+ {
+ $file = \IPS\File::create( 'nexus_Products', $image['filename'], $filedata );
+ $images[] = [ 'image_product' => $insertedId, 'image_location' => (string) $file, 'image_primary' => ( $default ? 0 : 1 ) ];
+ $default++;
+ }
+ catch( \Exception $e ) {}
+ }
+ }
+
+ /* Insert Images */
+ if( is_array( $images ) AND count( $images ) )
+ {
+ \IPS\Db::insert( 'nexus_package_images', $images );
+ }
+ }
+
+ /* Product Data */
+ if( $package['p_type'] == 'product' )
+ {
+ $product = array(
+ 'p_id' => $insertedId,
+ 'p_physical' => 0,
+ 'p_subscription' => 0,
+ 'p_shipping' => '*',
+ 'p_weight' => 0,
+ 'p_lkey' => 0,
+ 'p_lkey_identifier' => 'name',
+ 'p_lkey_uses' => -1,
+ 'p_show' => 1,
+ 'p_length' => 0,
+ 'p_width' => 0,
+ 'p_height' => 0
+ );
+
+ /* Save Product Data */
+ \IPS\Db::i()->insert( 'nexus_packages_products', $this->_getValues( $product, $info ) );
+
+ /* Add Link */
+ $this->software->app->addLink( $insertedId, $originalId, 'nexus_packages_products' );
+ }
+
+ /* Base Price */
+ if ( !\IPS\Db::i()->checkForColumn( 'nexus_package_base_prices', $currency ) )
+ {
+ \IPS\Db::i()->addColumn( 'nexus_package_base_prices', array(
+ 'name' => $currency,
+ 'type' => 'FLOAT'
+ ) );
+ }
+ \IPS\Db::i()->insert( 'nexus_package_base_prices', array( 'id' => $insertedId, $currency => $price->amount ) );
+
+ /* Custom lang strings */
+ \IPS\Lang::saveCustom( 'nexus', "nexus_package_{$insertedId}", $name );
+ \IPS\Lang::saveCustom( 'nexus', "nexus_package_{$insertedId}_desc", $description );
+
+ /* Add Link */
+ $this->software->app->addLink( $insertedId, $originalId, 'nexus_packages' );
+
+ /* Update associations */
+ \IPS\Db::i()->update( 'nexus_packages', array( 'p_associable' => $insertedId ), array( 'p_conv_associable=?', $originalId ) );
+
+ return $insertedId;
+ }
+
+ /**
+ * Convert an invoice
+ *
+ * @param array $info Info
+ * @return int|bool The ID of the newly inserted invoice, or FALSE on failure
+ */
+ public function convertNexusInvoice( $info )
+ {
+ /* !Required: ID */
+ if ( !isset( $info['i_id'] ) )
+ {
+ $this->software->app->log( 'nexus_invoices_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* !Required: Items */
+ if ( !isset( $info['i_items'] ) OR !is_array( $info['i_items'] ) )
+ {
+ $this->software->app->log( 'nexus_invoices_missing_items', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['i_id'] );
+ return FALSE;
+ }
+
+ /* Default Currency */
+ $currency = \IPS\nexus\Customer::loggedIn()->defaultCurrency();
+ $price = new \IPS\nexus\Money( $info['i_total'], $currency );
+ $info['i_total'] = $price->amount;
+
+ /* ITEMS */
+ $newItems = array();
+ foreach( $info['i_items'] as $item )
+ {
+ $item['itemID'] = $this->software->app->getLink( $item['itemID'], 'nexus_packages' );
+ $newItems[] = $item;
+ }
+ $info['i_items'] = json_encode( $newItems );
+
+ /* Find member */
+ try
+ {
+ $info['i_member'] = $this->software->app->getLink( $info['i_member'], 'core_members', TRUE );
+ }
+ catch( \Exception $e )
+ {
+ unset( $info['i_member'] );
+ }
+
+ if( isset( $info['i_status_extra'] ) )
+ {
+ $info['i_status_extra'] = json_encode( $info['i_status_extra'] );
+ }
+
+ /* Invoice */
+ $invoice = array(
+ 'i_status' => \IPS\nexus\Invoice::STATUS_PENDING,
+ 'i_title' => 'Converted Invoice '. $info['i_id'],
+ 'i_member' => 0,
+ 'i_items' => '[]',
+ 'i_total' => $price->amount,
+ 'i_date' => time(),
+ 'i_return_uri' => '',
+ 'i_paid' => 0,
+ 'i_status_extra' => '[]',
+ 'i_discount' => 0,
+ 'i_renewal_ids' => '',
+ 'i_po' => '',
+ 'i_notes' => NULL,
+ 'i_shipaddress' => NULL,
+ 'i_billaddress' => NULL,
+ 'i_currency' => $currency,
+ 'i_guest_data' => NULL,
+ 'i_billcountry' => NULL
+ );
+
+ /* Save Invoice Data */
+ $insertedId = \IPS\Db::i()->insert( 'nexus_invoices', $this->_getValues( $invoice, $info ) );
+
+ /* Add Link */
+ $this->software->app->addLink( $insertedId, $info['i_id'], 'nexus_invoices' );
+
+ return $insertedId;
+ }
+
+ /**
+ * Convert a transaction
+ *
+ * @param array $info Info
+ * @return int|bool The ID of the newly inserted transaction, or FALSE on failure
+ */
+ public function convertNexusTransaction( $info )
+ {
+ /* !Required: ID */
+ if ( !isset( $info['t_id'] ) )
+ {
+ $this->software->app->log( 'nexus_transactions_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* !Required: MemberID */
+ if ( !isset( $info['t_member'] ) )
+ {
+ $this->software->app->log( 'nexus_transactions_missing_member_id', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['t_id'] );
+ return FALSE;
+ }
+
+ /* !Required: InvoiceID */
+ if ( !isset( $info['t_invoice'] ) )
+ {
+ $this->software->app->log( 'nexus_transactions_missing_invoice_id', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['t_id'] );
+ return FALSE;
+ }
+
+ /* !Required: Amount */
+ if ( !isset( $info['t_amount'] ) )
+ {
+ $this->software->app->log( 'nexus_transactions_missing_amount', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['t_id'] );
+ return FALSE;
+ }
+
+ /* Map IDs */
+ $info['t_invoice'] = $this->software->app->getLink( $info['t_invoice'], 'nexus_invoices' );
+ try
+ {
+ $info['t_member'] = $this->software->app->getLink( $info['t_member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $ex)
+ {
+ $this->software->app->log( 'nexus_transactions_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['t_id'] );
+ return FALSE;
+ }
+
+ /* Default Currency */
+ $currency = \IPS\nexus\Customer::loggedIn()->defaultCurrency();
+ $price = new \IPS\nexus\Money( $info['t_amount'], $currency );
+ $info['t_amount'] = $price->amount;
+
+ $transaction = array(
+ 't_member' => 0,
+ 't_invoice' => 0,
+ 't_method' => 0,
+ 't_status' => \IPS\nexus\Transaction::STATUS_PENDING,
+ 't_amount' => 0,
+ 't_date' => time(),
+ 't_extra' => '[]',
+ 't_fraud' => '',
+ 't_gw_id' => '',
+ 't_ip' => '::1',
+ 't_fraud_blocked' => 0,
+ 't_currency' => $currency,
+ 't_partial_refund' => 0.000,
+ 't_auth' => NULL,
+ 't_billing_agreement' => NULL
+ );
+
+ /* Save Transaction Data */
+ $insertedId = \IPS\Db::i()->insert( 'nexus_transactions', $this->_getValues( $transaction, $info ) );
+
+ /* Add Link */
+ $this->software->app->addLink( $insertedId, $info['t_id'], 'nexus_transactions' );
+
+ return $insertedId;
+ }
+
+ /**
+ * Convert a purchase
+ *
+ * @param array $info Info
+ * @return int|bool The ID of the newly inserted purchase, or FALSE on failure
+ */
+ public function convertNexusPurchase( $info )
+ {
+ /* !Required: ID */
+ if ( !isset( $info['ps_id'] ) )
+ {
+ $this->software->app->log( 'nexus_purchases_missing_id', __METHOD__, \IPS\convert\App::LOG_WARNING );
+ return FALSE;
+ }
+
+ /* !Required: MemberID */
+ if ( !isset( $info['ps_member'] ) )
+ {
+ $this->software->app->log( 'nexus_purchases_missing_member_id', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['ps_id'] );
+ return FALSE;
+ }
+
+ /* !Required: InvoiceID */
+ if ( !isset( $info['ps_original_invoice'] ) )
+ {
+ $this->software->app->log( 'nexus_purchases_missing_invoice_id', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['ps_id'] );
+ return FALSE;
+ }
+
+ /* Map IDs */
+ $info['ps_original_invoice'] = $this->software->app->getLink( $info['ps_original_invoice'], 'nexus_invoices' );
+ $info['ps_item_id'] = $this->software->app->getLink( $info['ps_item_id'], 'nexus_packages' );
+
+ try
+ {
+ $info['ps_member'] = $this->software->app->getLink( $info['ps_member'], 'core_members', TRUE );
+ }
+ catch( \OutOfRangeException $ex)
+ {
+ $this->software->app->log( 'nexus_purchases_missing_member', __METHOD__, \IPS\convert\App::LOG_WARNING, $info['ps_id'] );
+ return FALSE;
+ }
+
+ /* Commission payments */
+ if( isset( $info['ps_pay_to'] ) )
+ {
+ $info['ps_pay_to'] = $this->software->app->getLink( $info['ps_pay_to'], 'core_members', TRUE );
+ }
+
+ /* Parent Purchases */
+ if( isset( $info['ps_parent'] ) )
+ {
+ try
+ {
+ $info['ps_parent'] = $this->software->app->getLink( $info['ps_parent'], 'nexus_purchases' );
+ }
+ catch( \OutOfRangeException $ex )
+ {
+ $info['ps_conv_parent'] = $info['ps_parent'];
+ unset( $info['ps_parent'] );
+ }
+ }
+
+ if( !isset( $info['ps_extra'] ) )
+ {
+ $info['ps_extra'] = array();
+ }
+
+ /* Old Primary Group */
+ if( isset( $info['ps_extra']['nexus']['old_primary_group'] ) )
+ {
+ $info['ps_extra']['nexus']['old_primary_group'] = $this->software->app->getLink( $info['ps_extra']['nexus']['old_primary_group'], 'core_groups', TRUE );
+ }
+
+ /* Old Secondary User Groups */
+ if( isset( $info['ps_extra']['nexus']['old_secondary_groups'] ) AND is_array( $info['ps_extra']['nexus']['old_secondary_groups'] ) )
+ {
+ $newSecondaryGroups = [];
+
+ array_walk( $info['ps_extra']['nexus']['old_secondary_groups'], function( $group, $key, &$newSecondaryGroups )
+ {
+ try
+ {
+ $newSecondaryGroups[] = $this->software->app->getLink( $group, 'core_groups', TRUE );
+ }
+ catch( \OutOfRangeException $ex ) { }
+ }, $newSecondaryGroups );
+
+ $info['ps_extra']['nexus']['old_secondary_groups'] = $newSecondaryGroups;
+ }
+
+ $info['ps_extra'] = json_encode( $info['ps_extra'] );
+
+ /* Default Currency */
+ $currency = \IPS\nexus\Customer::loggedIn()->defaultCurrency();
+ if( isset( $info['ps_renewal_price'] ) )
+ {
+ $price = new \IPS\nexus\Money( $info['ps_renewal_price'], $currency );
+ $info['ps_renewal_price'] = $price->amount;
+ }
+
+ $purchase = array(
+ 'ps_member' => 0,
+ 'ps_name' => 'Converted Purchase ' . $info['ps_id'],
+ 'ps_active' => 0,
+ 'ps_cancelled' => 0,
+ 'ps_start' => 0,
+ 'ps_expire' => 0,
+ 'ps_renewals' => 0,
+ 'ps_renewal_price' => 0.00,
+ 'ps_renewal_unit' => 0,
+ 'ps_app' => 'nexus',
+ 'ps_type' => 'package',
+ 'ps_item_id' => 0,
+ 'ps_item_uri' => '',
+ 'ps_admin_uri' => '',
+ 'ps_custom_fields' => '[]',
+ 'ps_extra' => '[]',
+ 'ps_parent' => 0,
+ 'ps_invoice_pending' => 0,
+ 'ps_invoice_warning_sent' => 1,
+ 'ps_pay_to' => NULL,
+ 'ps_commission' => NULL,
+ 'ps_original_invoice' => 0,
+ 'ps_tax' => 0,
+ 'ps_can_reactivate' => 1,
+ 'ps_grouped_renewals' => '',
+ 'ps_renewal_currency' => $currency,
+ 'ps_show' => 1,
+ 'ps_grace_period' => 0,
+ 'ps_billing_agreement' => NULL,
+ 'ps_conv_parent' => 0
+ );
+
+ /* Save Purchase Data */
+ $insertedId = \IPS\Db::i()->insert( 'nexus_purchases', $this->_getValues( $purchase, $info ) );
+
+ /* Add Link */
+ $this->software->app->addLink( $insertedId, $info['ps_id'], 'nexus_purchases' );
+
+ /* Update associations */
+ \IPS\Db::i()->update( 'nexus_purchases', array( 'ps_parent' => $insertedId ), array( 'ps_conv_parent=?', $info['ps_id'] ) );
+
+ return $insertedId;
+ }
+
+ /**
+ * Get values from an array that exist in the source array
+ *
+ * @param array $defaults Default populated array
+ * @param array $source User-supplied array
+ * @return array
+ */
+ protected function _getValues( array $defaults, array $source )
+ {
+ array_walk( $defaults, function( &$value, $key, $source )
+ {
+ if( isset( $source[ $key ] ) )
+ {
+ $value = $source[ $key ];
+ }
+ }, $source );
+
+ return $defaults;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software.php b/applications/convert/sources/Software.php
new file mode 100644
index 00000000..16d0e53b
--- /dev/null
+++ b/applications/convert/sources/Software.php
@@ -0,0 +1,442 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+abstract class _Software
+{
+ /**
+ * @brief \IPS\convert\App instance
+ */
+ public $app = NULL;
+
+ /**
+ * @brief \IPS\Db instance to the source application
+ */
+ public $db = NULL;
+
+ /**
+ * @brief Is UTF8
+ */
+ public $isUtfEight = TRUE;
+
+ /**
+ * @brief Flag to indicate the post data has been fixed during conversion, and we only need to use Legacy Parser
+ */
+ public static $contentFixed = FALSE;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @param bool If the database is needed or not.
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app, $needDB=TRUE )
+ {
+ $this->app = $app;
+
+ /* If we have a parent and no DB details we need to use that instead */
+ if ( $this->app->parent > 0 AND !$this->app->db_host )
+ {
+ $this->app->db_host = $this->app->_parent->db_host;
+ $this->app->db_port = $this->app->_parent->db_port;
+ $this->app->db_user = $this->app->_parent->db_user;
+ $this->app->db_pass = $this->app->_parent->db_pass;
+ $this->app->db_db = $this->app->_parent->db_db;
+ $this->app->db_prefix = $this->app->_parent->db_prefix;
+ $this->app->db_charset = $this->app->_parent->db_charset;
+ }
+
+ $connectionSettings = array(
+ 'sql_host' => $this->app->db_host,
+ 'sql_port' => $this->app->db_port,
+ 'sql_user' => $this->app->db_user,
+ 'sql_pass' => $this->app->db_pass,
+ 'sql_database' => $this->app->db_db,
+ 'sql_tbl_prefix' => $this->app->db_prefix,
+ );
+
+ if ( $this->app->db_charset === 'utf8mb4' )
+ {
+ $connectionSettings['sql_utf8mb4'] = TRUE;
+ }
+
+ //-----------------------------------------
+ // Are we connected?
+ // (in the great circle of life...)
+ //-----------------------------------------
+ /* If we are rebulding, we don't actually need to establish a connection to the source database. This will both help improve speed, as well as avoid an exception if the connection no longer works */
+ if ( $needDB )
+ {
+ try
+ {
+ $this->db = \IPS\Db::i( 'convert_' . $this->app->name, $connectionSettings );
+
+ /* Are we utf8? */
+ if ( !in_array( $this->app->db_charset, array( 'utf8', 'utf8mb4' ) ) )
+ {
+ /* Get all db charsets */
+ $charsets = static::getDatabaseCharsets( $this->db );
+
+ /* Set a flag that the data is not UTF8... we need to convert it to UTF8 at conversion time. */
+ if ( !in_array( mb_strtolower( $this->app->db_charset ), $charsets ) )
+ {
+ /* @todo We need to use a comprehensive conversion system like the UTF8 Converter... or maybe we can provide instructions on how to use it on non-IPS databases if MB can't do it? */
+ throw new \InvalidArgumentException( 'invalid_charset' );
+ }
+
+ $this->isUtfEight = FALSE;
+ $this->db->set_charset( $this->app->db_charset );
+ }
+ }
+ catch( \IPS\Db\Exception $e )
+ {
+ throw new \InvalidArgumentException( "Database Connection Failed: " . $e->getMessage() );
+ }
+ }
+ }
+
+ /**
+ * Magic __call() method
+ *
+ * @param string $name The method to call without convert prefix.
+ * @param mixed $arguements Arguments to pass to the method
+ * @return mixed
+ */
+ public function __call( $name, $arguments )
+ {
+ if ( method_exists( $this, 'convert' . $name ) )
+ {
+ return call_user_func( array( $this, 'convert' . $name ), $arguments );
+ }
+ elseif ( method_exists( $this, $name ) )
+ {
+ return call_user_func( $this, array( $name, $arguments ) );
+ }
+ else
+ {
+ \IPS\Log::log( "Call to undefined method in " . get_class( $this ) . "::{$name}", 'converters' );
+ return NULL;
+ }
+ }
+
+ /**
+ * Software Name
+ *
+ * @return string
+ * @throws \BadMethodCallException
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ throw new \BadMethodCallException( 'no_name' );
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ * #throws \BadMethodCallException
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ throw new \BadMethodCallException( 'no_key' );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return NULL|array
+ */
+ public static function parents()
+ {
+ return NULL;
+ }
+
+ /**
+ * Uses Prefix
+ *
+ * @return bool
+ */
+ public static function usesPrefix()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array|NULL
+ * @throws \BadMethodCallException
+ */
+ public static function canConvert()
+ {
+ /* Child classes must override this method */
+ throw new \BadMethodCallException( 'nothing_to_convert' );
+ }
+
+ /**
+ * Can we translate settings over to our IPS4 equivalents?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Settings Map
+ *
+ * @code
+ return array(
+ 'source_setting_key' => array( 'ips_setting_key' );
+ * @endcode
+ * @return array
+ */
+ public function settingsMap()
+ {
+ return array();
+ }
+
+ /**
+ * Settings Map List
+ *
+ * @code
+ return array(
+ 'source_setting_key' => array(
+ 'title' => 'Human Readable Name as presented in the source',
+ 'value' => 'The value from the source',
+ );
+ * @endcode
+ * @return array
+ */
+ public function settingsMapList()
+ {
+ return array();
+ }
+
+ /**
+ * List of Conversion Methods that require more information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array();
+ }
+
+ /**
+ * An array of steps which require more information. This method should return array like below if any steps require additional information. Otherwise, it should return NULL.
+ *
+ * @param string $method Conversion method
+ * @return array|NULL
+ *
+ * @code
+ return array(
+ 'convertAttachments' => array(
+ 'upload_path' => array( // The key is the name of the field, or the first parameter of the helper class constructor.
+ 'field_class' => 'IPS\\Helpers\\Form\\Text', // The form helper class for this field
+ 'field_default' => NULL, // The default value for this field.
+ 'field_required' => TRUE, // TRUE if this field is required.
+ 'field_extra' => array(), // Array of extra data that would normally go in $options for the form helper.
+ 'field_hint' => NULL, // A language key of hint text to display after the form (such as path to IPS Suite files). NULL for no additional text.
+ ),
+ )
+ );
+ * @endcode
+ */
+ public function getMoreInfo( $method )
+ {
+ return NULL;
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ try
+ {
+ $cacheKey = 'convert_' . $this->app->app_id . '_' . md5( $table . json_encode( $where ) );
+
+ if( !isset( \IPS\Data\Store::i()->$cacheKey ) OR $recache === TRUE )
+ {
+ \IPS\Data\Store::i()->$cacheKey = $this->db->select( 'COUNT(*)', $table, $where )->first();
+ }
+
+ return \IPS\Data\Store::i()->$cacheKey;
+ }
+ catch( \Exception $e )
+ {
+ throw new \IPS\convert\Exception( sprintf( \IPS\Member::loggedIn()->language()->get( 'could_not_count_rows' ), $table ) );
+ }
+ }
+
+ /**
+ * Get Library Class
+ *
+ * @param string|NULL $library Specific library to fetch
+ * @return \IPS\convert\Library
+ */
+ public function getLibrary( $library=NULL )
+ {
+ $library = $library ?: $this->app->sw;
+
+ if ( ! class_exists( 'IPS\\convert\\Library\\' . ucwords( $library ) ) )
+ {
+ throw new \InvalidArgumentException( 'invalid_library' );
+ }
+
+ $classname = 'IPS\\convert\\Library\\' . ucwords( $library );
+ return new $classname( $this );
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string|NULL
+ */
+ public static function getPreConversionInformation()
+ {
+ return NULL;
+ }
+
+ /**
+ * Fetch Remote Data
+ *
+ * @param string $table The table to selected data from
+ * @param string $idColumn The ID column to sort on, when not using keys.
+ * @param string|array|NULL $where WHERE clause for specific data to fetch.
+ * @param string $what What to fetch
+ *
+ * @return \IPS\Db\Select An \IPS\Db\Select object that can be further manipulated if necessary (e.g. joins)
+ */
+ public function fetch( $table, $idColumn='id', $where=NULL, $what='*' )
+ {
+ $libraryClass = $this->getLibrary();
+ if ( $libraryClass::$usingKeys === FALSE )
+ {
+ return $this->db->select( $what, $table, $where, $idColumn . ' ASC', array( $libraryClass::$startValue, $libraryClass::$perCycle ) );
+ }
+ else
+ {
+ if ( !isset( $_SESSION['currentKeyValue'] ) )
+ {
+ $libraryClass->setLastKeyValue( 0 );
+ }
+
+ $whereClause = array();
+ $whereClause[] = array( $libraryClass::$currentKeyName . '>?', $_SESSION['currentKeyValue'] );
+
+ if ( !is_null( $where ) )
+ {
+ if ( is_string( $where ) )
+ {
+ $whereClause[] = array( $where );
+ }
+ else
+ {
+ $whereClause[] = $where;
+ }
+ }
+
+ return $this->db->select( $what, $table, $whereClause, $libraryClass::$currentKeyName . ' ASC', array( 0, $libraryClass::$perCycle ) );
+ }
+ }
+
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ return array();
+ }
+
+ /**
+ * Updates Posted Content to work with the IPS4 parser.
+ *
+ * @param string The post
+ * @return string The converted post
+ */
+ public static function fixPostData( $post )
+ {
+ return $post;
+ }
+
+ /**
+ * Get database charsets
+ *
+ * @param \IPS\Db $database Database connection
+ * @return array
+ */
+ public static function getDatabaseCharsets( $database )
+ {
+ $charsets = array();
+ $result = $database->query( "SHOW CHARACTER SET;" );
+
+ while( $row = $result->fetch_assoc() )
+ {
+ $charsets[] = mb_strtolower( $row['Charset'] );
+ }
+
+ return $charsets;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Blog/Vbulletin.php b/applications/convert/sources/Software/Blog/Vbulletin.php
new file mode 100644
index 00000000..4a4b8bdc
--- /dev/null
+++ b/applications/convert/sources/Software/Blog/Vbulletin.php
@@ -0,0 +1,622 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Blog;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * @brief vBulletin 4 Stores all attachments under one table - this will store the content type for the blog app.
+ */
+ protected static $entryContentType = NULL;
+
+ /**
+ * @brief The schematic for vB3 and vB4 is similar enough that we can make specific concessions in a single converter for either version.
+ */
+ protected static $isLegacy = NULL;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @param bool Establish a DB connection
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app, $needDB=TRUE )
+ {
+ $return = parent::__construct( $app, $needDB );
+
+ /* Is this vB3 or vB4? */
+ try
+ {
+ if ( static::$isLegacy === NULL AND $needDB )
+ {
+ $version = $this->db->select( 'value', 'setting', array( "varname=?", 'templateversion' ) )->first();
+
+ if ( mb_substr( $version, 0, 1 ) == '3' )
+ {
+ static::$isLegacy = TRUE;
+ }
+ else
+ {
+ static::$isLegacy = FALSE;
+ }
+ }
+
+
+ /* If this is vB4, what is the content type ID for posts? */
+ if ( static::$entryContentType === NULL AND ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) AND $needDB )
+ {
+ static::$entryContentType = $this->db->select( 'contenttypeid', 'contenttype', array( "class=?", 'BlogEntry' ) )->first();
+ }
+ }
+ catch( \Exception $e ) {}
+
+ return $return;
+ }
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin Blog (3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ $attachmentTable = 'blog_attachment';
+ $attachmentWhere = NULL;
+ if ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) )
+ {
+ $attachmentTable = 'attachment';
+ $attachmentWhere = array( "contenttypeid=?", static::$entryContentType );
+ }
+
+ return array(
+ 'convertBlogs' => array(
+ 'table' => 'blog_user',
+ 'where' => NULL,
+ ),
+ 'convertBlogEntries' => array(
+ 'table' => 'blog',
+ 'where' => NULL
+ ),
+ 'convertBlogComments' => array(
+ 'table' => 'blog_text',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => $attachmentTable,
+ 'where' => $attachmentWhere
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ default:
+ return parent::countRows( $table, $where );
+ break;
+
+ case 'blog_text':
+ return parent::countRows( 'blog_text' ) - parent::countRows( 'blog' );
+ break;
+ }
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'blog_entries', 'class' => 'IPS\blog\Entry' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'blog_comments', 'class' => 'IPS\blog\Entry\Comment' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'blog_entries', 'class' => 'IPS\blog\Entry' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_blog_entries_rebuilding", "f_entry_comments_rebuilding", "f_blogs_recounting", "f_entry_tags_cache" );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin::fixPostData( $post );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array( 'convertAttachments' );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'file_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( $value != 'database' AND !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Convert blogs
+ *
+ * @return void
+ */
+ public function convertBlogs()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'bloguserid' );
+
+ foreach( $this->fetch( 'blog_user', 'bloguserid' ) as $blog )
+ {
+ try
+ {
+ $member = $this->db->select( '*', 'user', array( "userid=?", $blog['bloguserid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* User no longer exists, so skip */
+ $libraryClass->setLastKeyValue( $blog['bloguserid'] );
+ continue;
+ }
+
+ $socialgroup = NULL;
+
+ /* If this is a private blog, then we need to convert it to use a social group for invited users */
+ if ( $blog['options_member'] == 0 AND $blog['options_buddy'] > 0 )
+ {
+ /* Fetch users friends */
+ $friends = array();
+ foreach( $this->db->select( 'relationid', 'userlist', array( "userid=?", $blog['bloguserid'] ) ) AS $friend )
+ {
+ $friends[$friend] = $friend;
+ }
+
+ $socialgroup = array( 'members' => $friends );
+ }
+
+ $info = array(
+ 'blog_id' => $blog['bloguserid'],
+ 'blog_member_id' => $blog['bloguserid'],
+ 'blog_allowguests' => ( $blog['options_guest'] > 0 ) ? 1 : 0, # we don't care about specific options
+ 'blog_rating_total' => $blog['ratingtotal'],
+ 'blog_rating_count' => $blog['ratingnum'],
+ 'blog_settings' => array( 'allowrss' => TRUE ),
+ 'blog_name' => $blog['title'] ?: $member['username'] . "'s Blog",
+ 'blog_description' => $blog['description'] ?: '',
+ 'blog_last_edate' => $blog['lastblog'],
+ 'blog_count_entries' => $blog['entries'],
+ 'blog_count_comments' => $blog['comments'],
+ 'blog_count_entries_hidden' => $blog['deleted'],
+ 'blog_rating_average' => $blog['rating'],
+ 'blog_count_entries_future' => $blog['pending'],
+ );
+
+ $libraryClass->convertBlog( $info, $socialgroup );
+
+ foreach( $this->db->select( '*', 'blog_subscribeuser', array( "userid=?", $blog['bloguserid'] ) ) AS $follow )
+ {
+ $frequency = 'none';
+ if ( $follow['type'] == 'email' )
+ {
+ $frequency = 'immediate';
+ }
+
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'blog',
+ 'follow_area' => 'blogs',
+ 'follow_rel_id' => $blog['bloguserid'],
+ 'follow_rel_id_type' => 'blog_blogs',
+ 'follow_member_id' => $follow['userid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => $frequency,
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $blog['bloguserid'] );
+ }
+ }
+
+ /**
+ * Convert blog entries
+ *
+ * @return void
+ */
+ public function convertBlogEntries()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'blogid' );
+
+ foreach( $this->fetch( 'blog', 'blogid' ) AS $entry )
+ {
+ try
+ {
+ $text = $this->db->select( '*', 'blog_text', array( "blogtextid=?", $entry['firstblogtextid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $entry['blogid'] );
+ continue;
+ }
+
+ $lastcommenterid = 0;
+
+ try
+ {
+ $lastcommenterid = $this->db->select( 'userid', 'blog_text', array( "blogtextid=?", $entry['lastblogtextid'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ $status = 'published';
+ if ( $entry['state'] == 'draft' )
+ {
+ $status = 'draft';
+ }
+
+ $edittime = $editname = NULL;
+ try
+ {
+ $editlog = $this->db->select( 'dateline, username', 'blog_editlog', array( "blogtextid=?", $entry['firstblogtextid'] ), "dateline DESC" )->first();
+ $edittime = $editlog['dateline'];
+ $editname = $editlog['username'];
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'entry_id' => $entry['blogid'],
+ 'entry_blog_id' => $entry['userid'],
+ 'entry_name' => $text['title'],
+ 'entry_content' => $text['pagetext'],
+ 'entry_author_id' => $entry['postedby_userid'],
+ 'entry_author_name' => $entry['postedby_username'],
+ 'entry_date' => $entry['dateline'],
+ 'entry_status' => $status,
+ 'entry_num_comments' => $entry['comments_visible'],
+ 'entry_queued_comments' => $entry['comments_moderation'],
+ 'entry_views' => $entry['views'],
+ 'entry_hidden_comments' => $entry['comments_deleted'],
+ 'entry_last_comment_mid' => $lastcommenterid,
+ 'entry_edit_time' => $edittime,
+ 'entry_edit_name' => $editname,
+ 'entry_ip_address' => $text['ipaddress']
+ );
+
+ $libraryClass->convertBlogEntry( $info );
+
+ foreach( $this->db->select( '*', 'blog_rate', array( "blogid=?", $entry['blogid'] ) ) as $rating )
+ {
+ $libraryClass->convertRating( array(
+ 'id' => $rating['blograteid'],
+ 'class' => 'IPS\blog\Entry',
+ 'item_link' => 'blog_entries',
+ 'item_id' => $entry['blogid'],
+ 'rating' => $rating['vote'],
+ 'ip' => $rating['ipaddress'],
+ 'member' => $rating['userid'],
+ ) );
+ }
+
+ $categories = explode( ',', $entry['categories'] );
+ if ( count( $categories ) )
+ {
+ foreach( $categories as $category )
+ {
+ try
+ {
+ $data = $this->db->select( '*', 'blog_category', array( "blogcategoryid=?", $category ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'blog',
+ 'tag_meta_area' => 'blogs',
+ 'tag_meta_parent_id' => $entry['userid'],
+ 'tag_meta_id' => $entry['blogid'],
+ 'tag_text' => $data['title'],
+ 'tag_member_id' => $data['userid'],
+ 'tag_prefix' => 0
+ ) );
+ }
+ }
+
+ foreach( $this->db->select( '*', 'blog_subscribeentry', array( "blogid=?", $entry['blogid'] ) ) AS $follow )
+ {
+ $frequency = 'none';
+ if ( $follow['type'] == 'email' )
+ {
+ $frequency = 'immediate';
+ }
+
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'blog',
+ 'follow_area' => 'entries',
+ 'follow_rel_id' => $entry['blogid'],
+ 'follow_rel_id_type' => 'blog_entries',
+ 'follow_member_id' => $follow['userid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => $frequency,
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $entry['blogid'] );
+ }
+ }
+
+ /**
+ * Convert blog comments
+ *
+ * @return void
+ */
+ public function convertBlogComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'blogtextid' );
+
+ foreach( $this->fetch( 'blog_text', 'blogtextid' ) AS $comment )
+ {
+ /* Is this actually the entry? */
+ try
+ {
+ $entry = $this->db->select( '*', 'blog', array( "firstblogtextid=?", $comment['blogtextid'] ) )->first();
+
+ /* If that didn't throw an exception, then yes this is - skip */
+ $libraryClass->setLastKeyValue( $comment['blogtextid'] );
+ continue;
+ }
+ catch( \UnderflowException $e ) {} # nope, carry on
+
+ $edittime = $editname = NULL;
+ try
+ {
+ $editlog = $this->db->select( 'dateline, username', 'blog_editlog', array( "blogtextid=?", $comment['blogtextid'] ), "dateline DESC" )->first();
+ $edittime = $editlog['dateline'];
+ $editname = $editlog['username'];
+ }
+ catch( \UnderflowException $e ) {}
+
+ switch( $comment['state'] )
+ {
+ case 'moderation':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+
+ case 'visible':
+ default:
+ $approved = 1;
+ break;
+ }
+
+ $libraryClass->convertBlogComment( array(
+ 'comment_id' => $comment['blogtextid'],
+ 'comment_entry_id' => $comment['blogid'],
+ 'comment_text' => $comment['pagetext'],
+ 'comment_member_id' => $comment['userid'],
+ 'comment_member_name' => $comment['username'],
+ 'comment_ip_address' => $comment['ipaddress'],
+ 'comment_date' => $comment['dateline'],
+ 'comment_edit_time' => $edittime,
+ 'comment_edit_member_name' => $editname,
+ 'comment_edit_show' => 1,
+ 'comment_approved' => $approved
+ ) );
+
+ $libraryClass->setLastKeyValue( $comment['blogtextid'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'attachmentid' );
+
+ $where = NULL;
+ $column = NULL;
+
+ if ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) )
+ {
+ $where = array( "contenttypeid=?", static::$entryContentType );
+ $column = 'contentid';
+ $table = 'attachment';
+ }
+ else
+ {
+ $column = 'blogid';
+ $table = 'blog_attachment';
+ }
+
+ foreach( $this->fetch( $table, 'attachmentid', $where ) as $attachment )
+ {
+ if ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) )
+ {
+ $filedata = $this->db->select( '*', 'filedata', array( "filedataid=?", $attachment['filedataid'] ) )->first();
+ }
+ else
+ {
+ $filedata = $attachment;
+ $filedata['filedataid'] = $attachment['attachmentid'];
+ }
+
+ $map = array(
+ 'id1' => $attachment[$column],
+ 'id2' => NULL
+ );
+
+ $info = array(
+ 'attach_id' => $attachment['attachmentid'],
+ 'attach_file' => $attachment['filename'],
+ 'attach_date' => $attachment['dateline'],
+ 'attach_member_id' => $attachment['userid'],
+ 'attach_hits' => $attachment['counter'],
+ 'attach_ext' => $filedata['extension'],
+ 'attach_filesize' => $filedata['filesize'],
+ );
+
+ if ( $this->app->_session['more_info']['convertAttachments']['file_location'] == 'database' )
+ {
+ /* Simples! */
+ $data = $filedata['filedata'];
+ $path = NULL;
+ }
+ else
+ {
+ $data = NULL;
+ $path = implode( '/', preg_split( '//', $filedata['userid'], -1, PREG_SPLIT_NO_EMPTY ) );
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['file_location'], '/' ) . '/' . $path . '/' . $attachment['filedataid'] . '.attach';
+ }
+
+ $attach_id = $libraryClass->convertAttachment( $info, $map, $path, $data );
+
+ /* Do some re-jiggery on the post itself to make sure attachment displays */
+ $ourAttachment = \IPS\File::get( 'core_Attachment', \IPS\Db::i()->select( 'attach_location', 'core_attachments', array( 'attach_id=?', $attach_id ) )->first() );
+
+ $entry_id = $this->app->getLink( $attachment[ $column ], 'blog_entries' );
+
+ $post = \IPS\Db::i()->select( 'entry_content', 'blog_entries', array( "entry_id=?", $entry_id ) )->first();
+
+ if ( preg_match( "/\[ATTACH(.+?)?\]".$attachment['attachmentid']."\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH=(.+?)?\]" . $attachment['attachmentid'] . "\[\/ATTACH\]/i", '
', $post );
+ }
+ else
+ {
+ $post .= '
';
+ }
+
+ \IPS\Db::i()->update( 'blog_entries', array( 'entry_content' => $post ), array( "entry_id=?", $entry_id ) );
+
+ $libraryClass->setLastKeyValue( $attachment['attachmentid'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Calendar/Mybb.php b/applications/convert/sources/Software/Calendar/Mybb.php
new file mode 100644
index 00000000..33808155
--- /dev/null
+++ b/applications/convert/sources/Software/Calendar/Mybb.php
@@ -0,0 +1,211 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Calendar;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Mybb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "MyBB 1.8.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "mybb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCalendarCalendars' => array(
+ 'table' => 'calendars',
+ 'where' => NULL
+ ),
+ 'convertCalendarEvents' => array(
+ 'table' => 'events',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'mybb' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'calendar_events', 'class' => 'IPS\calendar\Event' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\calendar\Event' ), 3, array( 'class' ) );
+
+ return array( "f_rebuild_events", "f_recount_calendar" );
+ }
+
+ /**
+ * Convert calendars
+ *
+ * @return void
+ */
+ public function convertCalendarCalendars()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'cid' );
+
+ foreach( $this->fetch( 'calendars', 'cid' ) AS $row )
+ {
+ $libraryClass->convertCalendar( array(
+ 'cal_id' => $row['cid'],
+ 'cal_title' => $row['name'],
+ 'cal_position' => $row['disporder'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['cid'] );
+ }
+ }
+
+ /**
+ * Convert events
+ *
+ * @return void
+ */
+ public function convertCalendarEvents()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'eid' );
+
+ foreach( $this->fetch( 'events', 'eid' ) AS $row )
+ {
+ $repeats = \unserialize( $row['repeats'] );
+ $recurring = NULL;
+
+ switch( $repeats['repeats'] )
+ {
+ case 1: # daily
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'daily',
+ 'event_repeat_freq' => $repeats['days'],
+ );
+ break;
+
+ case 2: # Monday through Friday
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'daily',
+ 'event_repeat_freq' => 1,
+ 'repeat_freq_on_MO' => 1,
+ 'repeat_freq_on_TU' => 1,
+ 'repeat_freq_on_WE' => 1,
+ 'repeat_freq_on_TH' => 1,
+ 'repeat_freq_on_FR' => 1
+ );
+ break;
+
+ case 3: # weekly
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'weekly',
+ 'event_repeat_freq' => $repeats['weeks'],
+ 'repeat_freq_on_SU' => isset( $repeats['days'][0] ) ? 1 : 0,
+ 'repeat_freq_on_MO' => isset( $repeats['days'][1] ) ? 1 : 0,
+ 'repeat_freq_on_TU' => isset( $repeats['days'][2] ) ? 1 : 0,
+ 'repeat_freq_on_WE' => isset( $repeats['days'][3] ) ? 1 : 0,
+ 'repeat_freq_on_TH' => isset( $repeats['days'][4] ) ? 1 : 0,
+ 'repeat_freq_on_FR' => isset( $repeats['days'][5] ) ? 1 : 0,
+ 'repeat_freq_on_SA' => isset( $repeats['days'][6] ) ? 1 : 0,
+ );
+ break;
+
+ case 4: # monthly recurring, specific day (day 16 of every month)
+ /* We don't support specific day recurrence. */
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'monthly',
+ 'event_repeat_freq' => $repeats['months'],
+ );
+ break;
+
+ case 6: # yearly, specific day (October 16th, every year)
+ case 7: # yearly, specific day (Every third Friday of October)
+ /* We don't support specific days, and vBulletin doesn't support yearly frequencies more than 1 */
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'yearly',
+ 'event_repeat_freq' => 1,
+ );
+ break;
+ }
+
+ $libraryClass->convertCalendarEvent( array(
+ 'event_id' => $row['eid'],
+ 'event_calendar_id' => $row['cid'],
+ 'event_member_id' => $row['uid'],
+ 'event_title' => $row['name'],
+ 'event_content' => $row['description'],
+ 'event_saved' => $row['dateline'],
+ 'event_start_date' => \IPS\calendar\Date::ts( $row['starttime'] ),
+ 'event_end_date' => \IPS\calendar\Date::ts( $row['endtime'] ),
+ 'event_approved' => $row['visible'],
+ 'event_recurring' => $recurring,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['eid'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Calendar/Vbulletin.php b/applications/convert/sources/Software/Calendar/Vbulletin.php
new file mode 100644
index 00000000..b6e60538
--- /dev/null
+++ b/applications/convert/sources/Software/Calendar/Vbulletin.php
@@ -0,0 +1,250 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Calendar;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin Calendar (3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCalendarCalendars'=> array(
+ 'table' => 'calendar',
+ 'where' => NULL,
+ ),
+ 'convertCalendarEvents' => array(
+ 'table' => 'event',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'calendar_events', 'class' => 'IPS\calendar\Event' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\calendar\Event' ), 3, array( 'class' ) );
+
+ return array( "f_rebuild_events", "f_recount_calendar" );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin::fixPostData( $post );
+ }
+
+ /**
+ * Convert calendars
+ *
+ * @return void
+ */
+ public function convertCalendarCalendars()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'calendarid' );
+
+ foreach( $this->fetch( 'calendar', 'calendarid' ) AS $calendar )
+ {
+ $libraryClass->convertCalendar( array(
+ 'cal_id' => $calendar['calendarid'],
+ 'cal_title' => $calendar['title'],
+ 'cal_moderate' => $calendar['moderatenew'],
+ 'cal_position' => $calendar['displayorder']
+ ) );
+
+ $libraryClass->setLastkeyValue( $calendar['calendarid'] );
+ }
+ }
+
+ /**
+ * Convert events
+ *
+ * @return void
+ */
+ public function convertCalendarEvents()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'eventid' );
+
+ foreach( $this->fetch( 'event', 'eventid' ) AS $event )
+ {
+ /* We need to properly work out recurring, if possible - vBulletin stores basic recurring information versus our ICS Rules */
+ $recurring = NULL;
+ if ( $event['recurring'] )
+ {
+ switch( $event['recurring'] )
+ {
+ case 1: # daily recurring
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'daily',
+ 'event_repeat_freq' => $event['recuroption']
+ );
+ break;
+
+ case 2: # weekday recurring
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'daily',
+ 'event_repeat_freq' => 1,
+ 'repeat_freq_on_MO' => 1,
+ 'repeat_freq_on_TU' => 1,
+ 'repeat_freq_on_WE' => 1,
+ 'repeat_freq_on_TH' => 1,
+ 'repeat_freq_on_FR' => 1
+ );
+ break;
+
+ case 3: # weekly recurring
+ $option = explode( "|", $event['recuroption'] );
+
+ $daybits = array(
+ 'SU' => 1,
+ 'MO' => 2,
+ 'TU' => 4,
+ 'WE' => 8,
+ 'TH' => 16,
+ 'FR' => 32,
+ 'SA' => 64
+ );
+
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'weekly',
+ 'event_repeat_freq' => $option[0],
+ 'repeat_freq_on_SU' => ( $option[1] & $daybits['SU'] ) ? 1 : 0,
+ 'repeat_freq_on_MO' => ( $option[1] & $daybits['MO'] ) ? 1 : 0,
+ 'repeat_freq_on_TU' => ( $option[1] & $daybits['TU'] ) ? 1 : 0,
+ 'repeat_freq_on_WE' => ( $option[1] & $daybits['WE'] ) ? 1 : 0,
+ 'repeat_freq_on_TH' => ( $option[1] & $daybits['TH'] ) ? 1 : 0,
+ 'repeat_freq_on_FR' => ( $option[1] & $daybits['FR'] ) ? 1 : 0,
+ 'repeat_freq_on_SA' => ( $option[1] & $daybits['SA'] ) ? 1 : 0,
+ );
+ break;
+
+ case 4: # monthly recurring, specific day (day 16 of every month)
+ /* We don't support specific day recurrence. */
+ $option = explode( "|", $event['recuroption'] );
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'monthly',
+ 'event_repeat_freq' => $option[1],
+ );
+ break;
+
+ case 5: # monthly recurring, specific day (third Friday of every month)
+ /* We don't support specific day recurrence. */
+ $option = explode( "|", $event['recuroption'] );
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'monthly',
+ 'event_repeat_freq' => $option[2]
+ );
+ break;
+
+ case 6: # yearly, specific day (October 16th, every year)
+ case 7: # yearly, specific day (Every third Friday of October)
+ /* We don't support specific days, and vBulletin doesn't support yearly frequencies more than 1 */
+ $recurring = array(
+ 'event_repeat' => 1,
+ 'event_repeats' => 'yearly',
+ 'event_repeat_freq' => 1,
+ );
+ break;
+ }
+ }
+
+ $info = array(
+ 'event_id' => $event['eventid'],
+ 'event_calendar_id' => $event['calendarid'],
+ 'event_member_id' => $event['userid'],
+ 'event_title' => $event['title'],
+ 'event_content' => $event['event'],
+ 'event_saved' => $event['dateline'],
+ 'event_start_date' => \IPS\calendar\Date::ts( $event['dateline_from'] ),
+ 'event_end_date' => \IPS\calendar\Date::ts( $event['dateline_to'] ),
+ 'event_approved' => $event['visible'],
+ 'event_recurring' => $recurring
+ );
+
+ $libraryClass->convertCalendarEvent( $info );
+
+ $libraryClass->setLastKeyValue( $event['eventid'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Cms/Joomla.php b/applications/convert/sources/Software/Cms/Joomla.php
new file mode 100644
index 00000000..2c623942
--- /dev/null
+++ b/applications/convert/sources/Software/Cms/Joomla.php
@@ -0,0 +1,250 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Cms;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Joomla extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Joomla";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "joomla";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCmsDatabases' => array(
+ 'table' => 'cms_database',
+ 'where' => NULL
+ ),
+ 'convertCmsDatabaseCategories' => array(
+ 'table' => 'categories',
+ 'where' => NULL
+ ),
+ 'convertCmsDatabaseRecords' => array(
+ 'table' => 'content',
+ 'where' => NULL
+ ),
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'cms_database':
+ return 1;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Requires Parent?
+ *
+ * @return bool
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Available Parents
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'joomla' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ $database = $this->app->getLink( 1, 'cms_databases' );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\cms\Categories' . $database, 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'cms_custom_database_' . $database, 'class' => 'IPS\cms\Records' . $database ), 2, array( 'app', 'link', 'class' ) );
+
+ return array( "f_recount_cms_categories" );
+ }
+
+ /**
+ * Create database to store data in
+ *
+ * @return void
+ */
+ public function convertCmsDatabases()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass->convertCmsDatabase( array(
+ 'database_id' => 1,
+ 'database_name' => "Joomla Posts",
+ 'database_sln' => 'article',
+ 'database_pln' => 'articles',
+ 'database_scn' => 'Article',
+ 'database_pcn' => 'Articles',
+ 'database_ia' => 'an article',
+ 'database_tags_enabled' => 1,
+ ), array(
+ array(
+ 'field_id' => 1,
+ 'field_name' => 'Title',
+ 'field_type' => 'Text',
+ 'field_key' => 'post_title',
+ 'field_required' => 1,
+ 'field_position' => 1,
+ 'field_display_listing' => 1,
+ 'field_is_title' => 1,
+ ),
+ array(
+ 'field_id' => 2,
+ 'field_name' => 'Content',
+ 'field_type' => 'Editor',
+ 'field_key' => 'post_content',
+ 'field_required' => 1,
+ 'field_position' => 3,
+ 'field_is_content' => 1,
+ )
+ ) );
+
+ /* Throw an exception here to tell the library that we're done with this step */
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert categories
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'categories', 'id' ) as $row )
+ {
+ $info = array(
+ 'category_id' => $row['id'],
+ 'category_database_id' => 1,
+ 'category_name' => $row['title'],
+ 'category_furl_name' => $row['alias'],
+ 'category_desc' => $row['description'],
+ 'category_fields' => array( 'post_title', 'post_content' ),
+ );
+
+ $libraryClass->convertCmsDatabaseCategory( $info );
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert articles
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseRecords()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'content', 'id' ) as $row )
+ {
+ /* Set the basic details */
+ $info = array(
+ 'record_id' => $row['id'],
+ 'record_database_id' => 1,
+ 'member_id' => $row['created_by'],
+ 'record_allow_comments' => 1,
+ 'record_saved' => strtotime( $row['created'] ),
+ 'record_publish_date' => strtotime( $row['created'] ),
+ 'category_id' => $row['catid'],
+ 'record_approved' => ( $row['state'] != 0 ? 1 : 0 ),
+ 'record_updated' => ( strtotime( $row['modified'] ) > 0 ) ? strtotime( $row['modified'] ) : strtotime( $row['created'] ),
+ 'record_edit_member_id' => $row['modified_by'],
+ );
+
+ /* Joomla saves half of a post in introtext and half in fulltext */
+ if( isset( $row['introtext'] ) AND $row['introtext'] )
+ {
+ $row['fulltext'] = $row['fulltext'] ? $row['introtext'] . ' ' . $row['fulltext'] : $row['introtext'];
+ }
+
+ /* And then the custom fields */
+ $fields = array(
+ 1 => $row['title'],
+ 2 => $row['fulltext']
+ );
+
+ $libraryClass->convertCmsDatabaseRecord( $info, $fields );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Cms/Vbulletin.php b/applications/convert/sources/Software/Cms/Vbulletin.php
new file mode 100644
index 00000000..d943b76c
--- /dev/null
+++ b/applications/convert/sources/Software/Cms/Vbulletin.php
@@ -0,0 +1,473 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Cms;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin CMS (4.x only)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ $return = array(
+ 'convertCmsBlocks' => array(
+ 'table' => 'cms_widget',
+ 'where' => NULL,
+ ),
+ 'convertCmsPages' => array(
+ 'table' => 'page',
+ 'where' => NULL,
+ ),
+ 'convertCmsDatabases' => array(
+ 'table' => 'database',
+ 'where' => NULL,
+ ),
+ 'convertCmsDatabaseCategories' => array(
+ 'table' => 'cms_category',
+ 'where' => NULL,
+ ),
+ 'convertCmsDatabaseRecords' => array(
+ 'table' => 'cms_article',
+ 'where' => NULL,
+ ),
+ );
+
+ return $return;
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'cms_widget':
+ $blocksWeCanConvert = array();
+ foreach( $this->db->select( 'widgettypeid', 'cms_widgettype', array( \IPS\Db::i()->in( 'class', array( 'Rss', 'Static' ) ) ) ) AS $typeid )
+ {
+ $blocksWeCanConvert[] = $typeid;
+ }
+ return $this->db->select( 'COUNT(*)', 'cms_widget', array( $this->db->in( 'widgettypeid', $blocksWeCanConvert ) ) )->first();
+ break;
+
+ case 'page':
+ case 'database':
+ return 1;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin::fixPostData( $post );
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_vb_preconvert';
+ }
+
+ /**
+ * Get Setting Value - useful for global settings that need to be translated to group or member settings
+ *
+ * @param string $key The setting key
+ * @return mixed
+ */
+ protected function _setting( $key )
+ {
+ if ( isset( $this->settingsCache[$key] ) )
+ {
+ return $this->settingsCache[$key];
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'value, defaultvalue', 'setting', array( "varname=?", $key ) )->first();
+
+ if ( $setting['value'] )
+ {
+ $this->settingsCache[$key] = $setting['value'];
+ }
+ else
+ {
+ $this->settingsCache[$key] = $setting['defaultvalue'];
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If we failed to find it, we probably will fail again on later attempts */
+ $this->settingsCache[$key] = NULL;
+ }
+
+ return $this->settingsCache[$key];
+ }
+
+ /**
+ * Convert CMS blocks
+ *
+ * @return void
+ */
+ public function convertCmsBlocks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'widgetid' );
+
+ /* We CAN bring over some blocks, like static widgets */
+ $blocksWeCanConvert = array();
+ $rssTypeId = NULL;
+ foreach( $this->db->select( 'widgettypeid, class', 'cms_widgettype', array( \IPS\Db::i()->in( 'class', array( 'Rss', 'Static' ) ) ) ) AS $type )
+ {
+ if ( $type['class'] == 'Rss' )
+ {
+ $rssTypeId = $type['widgettypeid'];
+ }
+ $blocksWeCanConvert[] = $type['widgettypeid'];
+ }
+
+ foreach( $this->fetch( 'cms_widget', 'widgetid', array( \IPS\Db::i()->in( 'widgettypeid', $blocksWeCanConvert ) ) ) AS $block )
+ {
+ $config = $this->db->select( 'name, value', 'cms_widgetconfig', array( "widgetid=?", $block['widgetid'] ) )->setKeyField( 'name' )->setValueField( 'value' )->first();
+
+ $info = array(
+ 'block_id' => $block['widgetid'],
+ 'block_name' => $block['title'],
+ 'block_description' => $block['description'],
+ 'block_plugin' => ( $block['widgettypeid'] == $rssTypeId ) ? 'Rss' : NULL,
+ 'block_plugin_config' => ( $block['widgettypeid'] == $rssTypeId ) ? array(
+ 'block_rss_import_title' => $block['title'],
+ 'block_rss_import_url' => $config['url'],
+ 'block_rss_import_number' => $config['max_items'],
+ 'block_rss_import_cache' => 30,
+ 'block_type' => 'plugin',
+ 'block_editor' => 'html',
+ 'block_plugin' => 'Rss',
+ 'block_plugin_app' => 'cms',
+ 'template_params' => '',
+ 'type' => 'plugin',
+ 'plugin_app' => 'cms',
+ ) : NULL,
+ 'block_content' => ( $block['widgettypeid'] != $rssTypeId ) ? $config['statichtml'] : NULL,
+ );
+
+ $libraryClass->convertCmsBlock( $info );
+
+ $libraryClass->setLastKeyValue( $block['widgetid'] );
+ }
+ }
+
+ /**
+ * Create a CMS page
+ *
+ * @return void
+ */
+ public function convertCmsPages()
+ {
+ $this->getLibrary()->convertCmsPage( array(
+ 'page_id' => 1,
+ 'page_name' => 'vBulletin Articles',
+ ) );
+
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Create a database
+ *
+ * @return void
+ */
+ public function convertCmsDatabases()
+ {
+ $convertedForums = FALSE;
+ try
+ {
+ $this->app->checkForSibling( 'forums' );
+
+ $convertedForums = TRUE;
+ }
+ catch( \OutOfRangeException $e ) {}
+ $this->getLibrary()->convertCmsDatabase( array(
+ 'database_id' => 1,
+ 'database_name' => 'vBulletin Articles',
+ 'database_sln' => 'article',
+ 'database_pln' => 'articles',
+ 'database_scn' => 'Article',
+ 'database_pcn' => 'Articles',
+ 'database_ia' => 'an article',
+ 'database_record_count' => $this->db->select( 'COUNT(*)', 'cms_article' )->first(),
+ 'database_tags_enabled' => 1,
+ 'database_forum_record' => ( $convertedForums ) ? 1 : 0,
+ 'database_forum_comments' => ( $convertedForums ) ? 1 : 0,
+ 'database_forum_delete' => ( $convertedForums ) ? 1 : 0,
+ 'database_forum_prefix' => ( $convertedForums ) ? 'Article: ' : '',
+ 'database_forum_forum' => ( $convertedForums ) ? $this->_setting( 'vbcmsforumid' ) : 0,
+ 'database_page_id' => 1,
+ ), array(
+ array(
+ 'field_id' => 1,
+ 'field_type' => 'Text',
+ 'field_name' => 'Title',
+ 'field_key' => 'article_title',
+ 'field_required' => 1,
+ 'field_user_editable' => 1,
+ 'field_position' => 1,
+ 'field_display_listing' => 1,
+ 'field_display_display' => 1,
+ 'field_is_title' => TRUE,
+ ),
+ array(
+ 'field_id' => 2,
+ 'field_type' => 'Editor',
+ 'field_name' => 'Content',
+ 'field_key' => 'article_content',
+ 'field_required' => 1,
+ 'field_user_editable' => 1,
+ 'field_position' => 2,
+ 'field_display_listing' => 0,
+ 'field_display_display' => 1,
+ 'field_is_content' => TRUE
+ )
+ ) );
+
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert CMS database categories
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'categoryid' );
+
+ foreach( $this->fetch( 'cms_category', 'categoryid' ) AS $row )
+ {
+ $libraryClass->convertCmsDatabaseCategory( array(
+ 'category_id' => $row['categoryid'],
+ 'category_database_id' => 1,
+ 'category_name' => $row['category'],
+ 'category_desc' => $row['description'],
+ 'category_position' => $row['catleft'],
+ 'category_fields' => array( 'article_title', 'article_content' )
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['categoryid'] );
+ }
+ }
+
+ /**
+ * Convert CMS database records
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseRecords()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'contentid' );
+
+ foreach( $this->fetch( 'cms_article', 'contentid' ) AS $row )
+ {
+ try
+ {
+ $node = $this->db->select( '*', 'cms_node', array( "contenttypeid=? AND contentid=?", $this->_articleContentType(), $row['contentid'] ) )->first();
+ $nodeinfo = $this->db->select( '*', 'cms_nodeinfo', array( "nodeid=?", $node['nodeid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $row['contentid'] );
+ continue;
+ }
+
+ try
+ {
+ $category = $this->db->select( 'categoryid', 'cms_nodecategory', array( "nodeid=?", $node['nodeid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Create one */
+ try
+ {
+ $this->app->getLink( '__orphan__', 'cms_database_categories' );
+
+ $category = '__orphan__';
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertCmsDatabaseCategory( array(
+ 'category_id' => '__orphan__',
+ 'category_database_id' => 1,
+ 'category_name' => "vBulletin Articles",
+ 'category_fields' => array( 'article_title', 'article_content' )
+ ) );
+
+ $category = '__orphan__';
+ }
+ }
+
+ $keywords = array();
+ foreach( explode( ',', $nodeinfo['keywords'] ) AS $word )
+ {
+ $keywords[] = trim( $word );
+ }
+
+ $libraryClass->convertCmsDatabaseRecord( array(
+ 'record_id' => $row['contentid'],
+ 'record_database_id' => 1,
+ 'member_id' => $node['userid'],
+ 'rating_real' => $nodeinfo['ratingtotal'],
+ 'rating_hits' => $nodeinfo['ratingnum'],
+ 'rating_value' => $nodeinfo['rating'],
+ 'record_locked' => $node['comments_enabled'],
+ 'record_views' => $nodeinfo['viewcount'],
+ 'record_allow_comments' => $node['comments_enabled'],
+ 'record_saved' => $node['publishdate'],
+ 'record_updated' => $node['lastupdated'],
+ 'category_id' => $category,
+ 'record_approved' => ( $node['hidden'] ) ? -1 : 1,
+ 'record_static_furl' => $node['url'],
+ 'record_meta_keywords' => $keywords,
+ 'record_meta_description' => $nodeinfo['description'],
+ 'record_topicid' => $nodeinfo['associatedthreadid'],
+ 'record_publish_date' => $node['publishdate'],
+ ), array(
+ 1 => $nodeinfo['title'],
+ 2 => $row['pagetext']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['contentid'] );
+ }
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ foreach( \IPS\Db::i()->select( 'ipb_id', 'convert_link', array( 'type=? AND app=?', 'cms_databases', $this->app->app_id ) ) as $database )
+ {
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\cms\Categories' . $database, 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\cms\Records'. $database ), 3, array( 'class' ) );
+
+ // This converter doesn't support tags at this time, uncomment when it does.
+ //\IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'cms_custom_database_' . $database, 'class' => 'IPS\cms\Records' . $database ), 3, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'cms_custom_database_' . $database, 'class' => 'IPS\cms\Records' . $database ), 2, array( 'app', 'link', 'class' ) );
+ }
+
+ return array( "f_recount_cms_categories", "f_rebuild_cms_tags" );
+ }
+
+ /**
+ * @brief Cached article content type
+ */
+ protected $_articleContentType = NULL;
+
+ /**
+ * Return the article content type
+ *
+ * @return void
+ */
+ protected function _articleContentType()
+ {
+ if ( $this->_articleContentType === NULL )
+ {
+ try
+ {
+ $this->_articleContentType = $this->db->select( 'contenttypeid', 'contenttype', array( "class=?", 'Article' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $this->_articleContentType = 24; # default
+ }
+ }
+
+ return $this->_articleContentType;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Cms/Wordpress.php b/applications/convert/sources/Software/Cms/Wordpress.php
new file mode 100644
index 00000000..b6d13f21
--- /dev/null
+++ b/applications/convert/sources/Software/Cms/Wordpress.php
@@ -0,0 +1,489 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Cms;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Wordpress extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Wordpress (4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "wordpress";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCmsPages' => array(
+ 'table' => 'posts',
+ 'where' => array( "post_type=?", 'page' )
+ ),
+ 'convertCmsDatabases' => array(
+ 'table' => 'cms_database',
+ 'where' => NULL
+ ),
+ 'convertCmsDatabaseCategories' => array(
+ 'table' => 'term_taxonomy',
+ 'where' => array( "taxonomy=?", 'category' )
+ ),
+ 'convertCmsDatabaseRecords' => array(
+ 'table' => 'posts',
+ 'where' => array( "post_type=?", 'post' ),
+ ),
+ 'convertCmsDatabaseComments' => array(
+ 'table' => 'comments',
+ 'where' => NULL
+ ),
+ 'convertCmsMedia' => array(
+ 'table' => 'posts',
+ 'where' => array( "post_type=?", 'attachment' )
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'cms_database':
+ return 1;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Requires Parent?
+ *
+ * @return bool
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Available Parents
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'wordpress' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertCmsMedia',
+ 'convertCmsDatabaseRecords'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertCmsDatabaseRecords':
+ $return['convertCmsDatabaseRecords']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_required' => TRUE,
+ 'field_default' => NULL,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'convert_wp_typical_path' ),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+
+ case 'convertCmsMedia':
+ $return['convertCmsMedia']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_required' => TRUE,
+ 'field_default' => NULL,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'convert_wp_typical_path' ),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ $database = $this->app->getLink( 1, 'cms_databases' );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\cms\Categories' . $database, 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\cms\Records'. $database ), 3, array( 'class' ) );
+
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'cms_records', 'class' => 'IPS\cms\Records' . $database ), 3, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'cms_custom_database_' . $database, 'class' => 'IPS\cms\Records' . $database ), 2, array( 'app', 'link', 'class' ) );
+
+ return array( "f_recount_cms_categories", "f_rebuild_cms_tags" );
+ }
+
+ /**
+ * Convert CMS pages
+ *
+ * @return void
+ */
+ public function convertCmsPages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'ID' );
+
+ foreach( $this->fetch( 'posts', 'ID', array( "post_type=?", 'page' ) ) AS $row )
+ {
+ $libraryClass->convertCmsPage( array(
+ 'page_id' => $row['ID'],
+ 'page_name' => $row['post_title'],
+ 'page_seo_name' => $row['post_name'],
+ 'page_content' => $row['post_content'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+
+ /**
+ * Convert CMS databases
+ *
+ * @return void
+ */
+ public function convertCmsDatabases()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass->convertCmsDatabase( array(
+ 'database_id' => 1,
+ 'database_name' => "WordPress Posts",
+ 'database_sln' => 'post',
+ 'database_pln' => 'posts',
+ 'database_scn' => 'Post',
+ 'database_pcn' => 'Posts',
+ 'database_ia' => 'a post',
+ 'database_tags_enabled' => 1,
+ ), array(
+ array(
+ 'field_id' => 1,
+ 'field_name' => 'Title',
+ 'field_type' => 'Text',
+ 'field_key' => 'post_title',
+ 'field_required' => 1,
+ 'field_position' => 1,
+ 'field_display_listing' => 1,
+ 'field_is_title' => 1,
+ ),
+ array(
+ 'field_id' => 2,
+ 'field_name' => 'Content',
+ 'field_type' => 'Editor',
+ 'field_key' => 'post_content',
+ 'field_required' => 1,
+ 'field_position' => 3,
+ 'field_is_content' => 1,
+ )
+ ) );
+
+ /* Throw an exception here to tell the library that we're done with this step */
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert CMS database categories
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'term_taxonomy.term_taxonomy_id' );
+
+ foreach( $this->fetch( 'term_taxonomy', 'term_taxonomy.term_taxonomy_id', array( "term_taxonomy.taxonomy=?", 'category' ) )->join( 'terms', 'terms.term_id = term_taxonomy.term_id' ) AS $row )
+ {
+ $info = array(
+ 'category_id' => $row['term_taxonomy_id'],
+ 'category_database_id' => 1,
+ 'category_name' => $row['name'],
+ 'category_furl_name' => $row['slug'],
+ 'category_fields' => array( 'post_title', 'post_content' ),
+ );
+
+ $libraryClass->convertCmsDatabaseCategory( $info );
+ $libraryClass->setLastKeyValue( $row['term_taxonomy_id'] );
+ }
+ }
+
+ /**
+ * Convert CMS database records
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseRecords()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'ID' );
+
+ foreach( $this->fetch( 'posts', 'ID', array( "post_type=?", 'post' ) ) AS $row )
+ {
+ /* We only support one category per record - in this instance, fetch them all, pop off the last one, then convert the rest as tags later. */
+ $categories = array();
+ foreach( $this->db->select( '*', 'term_relationships', array( "term_relationships.object_id=? AND term_taxonomy.taxonomy=?", $row['ID'], 'category' ) )->join( 'term_taxonomy', 'term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id' ) AS $term )
+ {
+ $categories[] = $term['term_taxonomy_id'];
+ }
+ $category = array_pop( $categories );
+
+ /* Post Meta */
+ $meta = array();
+ foreach( $this->db->select( '*', 'postmeta', array( "post_id=?", $row['ID'] ) ) AS $m )
+ {
+ $meta[$m['meta_key']] = $m['meta_value'];
+ }
+
+ /* Work out wacky approved status */
+ switch( $row['post_status'] )
+ {
+ case 'publish':
+ $approved = 1;
+ break;
+
+ case 'auto-draft':
+ case 'draft':
+ $approved = 0;
+ break;
+
+ case 'trash':
+ case 'private':
+ $approved = -1;
+ break;
+
+ default:
+ $approved = 0; # play it on the safe side.
+ break;
+ }
+
+ /* Record Image */
+ $image = NULL;
+ if ( isset( $meta['_thumbnail_id'] ) )
+ {
+ try
+ {
+ $location = $this->db->select( 'meta_value', 'postmeta', array( "post_id=? AND meta_key=?", $meta['_thumbnail_id'], '_wp_attached_file' ) )->first();
+ $image = rtrim( $this->app->_session['more_info']['convertCmsDatabaseRecords']['file_location'], '/' ) . '/' . $location;
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $info = array(
+ 'record_id' => $row['ID'],
+ 'record_database_id' => 1,
+ 'member_id' => $row['post_author'],
+ 'record_locked' => ( $row['comment_status'] == 'closed' ) ? 1 : 0,
+ 'record_comments' => $row['comment_count'],
+ 'record_allow_comments' => 1,
+ 'record_saved' => new \IPS\DateTime( $row['post_date'] ),
+ 'record_updated' => new \IPS\DateTime( $row['post_modified'] ),
+ 'category_id' => $category,
+ 'record_approved' => $approved,
+ 'record_dynamic_furl' => $row['post_name'],
+ 'record_static_furl' => $row['post_name'],
+ 'record_publish_date' => new \IPS\DateTime( $row['post_date'] ),
+ 'record_image' => $image,
+ );
+
+ $fields = array(
+ 1 => $row['post_title'],
+ 2 => $row['post_content']
+ );
+
+ $libraryClass->convertCmsDatabaseRecord( $info, $fields );
+
+ /* Tags */
+ $tags = array();
+
+ /* ... from categories */
+ foreach( $this->db->select( 'term_id', 'term_taxonomy', array( $this->db->in( 'term_taxonomy_id', $categories ) ) ) AS $cat )
+ {
+ $text = $this->db->select( 'name', 'terms', array( "term_id=?", $cat ) )->first();
+ $tags[] = $text;
+ }
+
+ /* ... from actual tags */
+ foreach( $this->db->select( '*', 'term_relationships', array( "term_relationships.object_id=? AND term_taxonomy.taxonomy=?", $row['ID'], 'post_tag' ) )->join( 'term_taxonomy', 'term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id' ) AS $term )
+ {
+ $text = $this->db->select( 'name', 'terms', array( "term_id=?", $term['term_id'] ) )->first();
+ $tags[] = $text;
+ }
+
+ /* Now convert them... we need the database ID. */
+ $database = $this->app->getLink( 1, 'cms_databases' );
+ foreach( $tags AS $tag )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'cms',
+ 'tag_meta_area' => "records{$database}",
+ 'tag_meta_parent_id' => $category,
+ 'tag_meta_id' => $row['ID'],
+ 'tag_text' => $tag,
+ 'tag_member_id' => $row['post_author'],
+ 'tag_prefix' => 0,
+ 'tag_meta_link' => 'cms_custom_database_' . $database,
+ 'tag_meta_parent_link' => 'cms_database_categories',
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+
+ /**
+ * Convert CMS database comments
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'comment_ID' );
+
+ foreach( $this->fetch( 'comments', 'comment_ID' ) AS $row )
+ {
+ switch( $row['comment_approved'] )
+ {
+ case 1:
+ $approved = 1;
+ break;
+
+ case 0:
+ $approved = 0;
+ break;
+
+ case 'trash':
+ case 'spam':
+ $approved = -1;
+ break;
+ }
+
+ $libraryClass->convertCmsDatabaseComment( array(
+ 'comment_id' => $row['comment_ID'],
+ 'comment_database_id' => 1,
+ 'comment_record_id' => $row['comment_post_ID'],
+ 'comment_date' => new \IPS\DateTime( $row['comment_date'] ),
+ 'comment_ip_address' => $row['comment_author_IP'],
+ 'comment_user' => $row['user_id'],
+ 'comment_author' => $row['comment_author'],
+ 'comment_approved' => $approved,
+ 'comment_post' => $row['comment_content'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['comment_ID'] );
+ }
+ }
+
+ /**
+ * Convert CMS media
+ *
+ * @return void
+ */
+ public function convertCmsMedia()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'ID' );
+
+ foreach( $this->fetch( 'posts', 'ID', array( "post_type=?", 'attachment' ) ) AS $row )
+ {
+ $meta = array();
+ foreach( $this->db->select( '*', 'postmeta', array( "post_id=?", $row['ID'] ) ) AS $m )
+ {
+ $meta[$m['meta_key']] = $m['meta_value'];
+ }
+
+ $filename = explode( '/', $meta['_wp_attached_file'] );
+ $filename = array_pop( $filename );
+
+ $path = rtrim( $this->app->_session['more_info']['convertCmsMedia']['file_location'], '/' );
+
+ $info = array(
+ 'media_id' => $row['ID'],
+ 'media_filename' => $filename,
+ 'media_added' => new \IPS\DateTime( $row['post_date'] ),
+ );
+
+ $libraryClass->convertCmsMedia( $info, $path . '/' . $meta['_wp_attached_file'] );
+
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Cms/Xenforo.php b/applications/convert/sources/Software/Cms/Xenforo.php
new file mode 100644
index 00000000..64dbab94
--- /dev/null
+++ b/applications/convert/sources/Software/Cms/Xenforo.php
@@ -0,0 +1,140 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Cms;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenforo extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "XenForo Page Nodes";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "xenforo";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCmsPages' => array(
+ 'table' => 'xf_page',
+ 'where' => NULL,
+ )
+ );
+ }
+
+ /**
+ * Uses Prefix
+ *
+ * @return bool
+ */
+ public static function usesPrefix()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'xenforo' ) );
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase string
+ * @return string|null Phrase
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Convert CMS page
+ *
+ * @return void
+ */
+ public function convertCmsPages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'xf_page.node_id' );
+
+ foreach( $this->fetch( 'xf_page', 'xf_page.node_id' )->join( 'xf_node', 'xf_node.node_id = xf_page.node_id' ) AS $row )
+ {
+ try
+ {
+ $template = $this->db->select( 'template', 'xf_template', array( "title=?", "_page_node.{$row['node_id']}" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $template = '';
+ }
+
+ $libraryClass->convertCmsPage( array(
+ 'page_id' => $row['node_id'],
+ 'page_name' => $row['title'],
+ 'page_seo_name' => $row['node_name'],
+ 'page_content' => $template,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['node_id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Cms/Xenfororm.php b/applications/convert/sources/Software/Cms/Xenfororm.php
new file mode 100644
index 00000000..0d049887
--- /dev/null
+++ b/applications/convert/sources/Software/Cms/Xenfororm.php
@@ -0,0 +1,288 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Cms;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenfororm extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "XenForo Resource Manager Articles";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "xenfororm";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertCmsDatabases' => array(
+ 'table' => 'cms_databases',
+ 'where' => NULL,
+ ),
+ 'convertCmsDatabaseCategories' => array(
+ 'table' => 'xf_resource_category',
+ 'where' => array( "allow_fileless=?", 1 )
+ ),
+ 'convertCmsDatabaseRecords' => array(
+ 'table' => 'xf_resource',
+ 'where' => array( "is_fileless=?", 1 )
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'cms_databases':
+ return 1;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Uses Prefix
+ *
+ * @return bool
+ */
+ public static function usesPrefix()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'xenforo' ) );
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase
+ * @return string|null
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Convert CMS databases
+ *
+ * @return void
+ */
+ public function convertCmsDatabases()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass->convertCmsDatabase( array(
+ 'database_id' => 1,
+ 'database_name' => "Resources",
+ 'database_sln' => 'resource',
+ 'database_pln' => 'resources',
+ 'database_scn' => 'Resource',
+ 'database_pcn' => 'Resources',
+ 'database_ia' => 'a resource',
+ ), array(
+ array(
+ 'field_id' => 1,
+ 'field_name' => 'Title',
+ 'field_type' => 'Text',
+ 'field_key' => 'resource_title',
+ 'field_required' => 1,
+ 'field_position' => 1,
+ 'field_display_listing' => 1,
+ 'field_is_title' => 1,
+ ),
+ array(
+ 'field_id' => 2,
+ 'field_name' => 'Tag Line',
+ 'field_type' => 'Text',
+ 'field_key' => 'resource_tagline',
+ 'field_required' => 1,
+ 'field_position' => 2,
+ ),
+ array(
+ 'field_id' => 3,
+ 'field_name' => 'Content',
+ 'field_type' => 'Editor',
+ 'field_key' => 'resource_content',
+ 'field_required' => 1,
+ 'field_position' => 3,
+ 'field_is_content' => 1,
+ )
+ ) );
+
+ /* Throw an exception here to tell the library that we're done with this step */
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert CMS database categories
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'resource_category_id' );
+
+ foreach( $this->fetch( 'xf_resource_category', 'resource_category_id', array( 'allow_fileless=?', 1 ) ) AS $row )
+ {
+ $forumoverride = 0;
+ $forumid = 0;
+ if ( $row['thread_node_id'] )
+ {
+ $forumoverride = 1;
+ $forumid = $row['thread_node_id'];
+ }
+
+ $info = array(
+ 'category_id' => $row['resource_category_id'],
+ 'category_database_id' => 1,
+ 'category_name' => $row['category_title'],
+ 'category_position' => $row['display_order'],
+ 'category_fields' => array( 'resource_title', 'resource_tagline', 'resource_content' ),
+ );
+
+ $libraryClass->convertCmsDatabaseCategory( $info );
+
+ $libraryClass->setLastKeyValue( $row['resource_category_id'] );
+ }
+ }
+
+ /**
+ * Convert CMS database records
+ *
+ * @return void
+ */
+ public function convertCmsDatabaseRecords()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'resource_id' );
+
+ foreach( $this->fetch( 'xf_resource', 'resource_id', array( 'is_fileless=?', 1 ) ) AS $row )
+ {
+ $post = $this->db->select( 'message, post_date', 'xf_resource_update', array( "resource_id=?", $row['resource_id'] ), "post_date DESC" )->first();
+
+ switch( $row['resource_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderated':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ $info = array(
+ 'record_id' => $row['resource_id'],
+ 'record_database_id' => 1,
+ 'member_id' => $row['user_id'],
+ 'record_saved' => $row['resource_date'],
+ 'record_updated' => $post['post_date'],
+ 'category_id' => $row['resource_category_id'],
+ 'record_approved' => $approved,
+ 'record_topicid' => $row['discussion_thread_id'],
+ 'record_publish_date' => $row['resource_date'],
+ );
+
+ $fields = array( 1 => $row['title'], 2 => $row['tag_line'], 3 => $post['message'] );
+
+ $libraryClass->convertCmsDatabaseRecord( $info, $fields );
+
+ $libraryClass->setLastKeyValue( $row['resource_id'] );
+ }
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ $database = $this->app->getLink( 1, 'cms_databases' );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\cms\Categories' . $database, 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'cms_custom_database_' . $database, 'class' => 'IPS\cms\Records' . $database ), 2, array( 'app', 'link', 'class' ) );
+
+ return array( "f_recount_cms_categories", "f_rebuild_cms_tags" );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Expressionengine.php b/applications/convert/sources/Software/Core/Expressionengine.php
new file mode 100644
index 00000000..4309de0e
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Expressionengine.php
@@ -0,0 +1,722 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 14 June 2016
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Expressionengine extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ return "Expression Engine";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ return 'expressionengine';
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertProfileFields' => array(
+ 'table' => 'exp_member_fields',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'exp_member_groups',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'exp_members',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'exp_members',
+ 'where' => array( "sig_img_filename<>''" )
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'exp_message_data',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'exp_message_data',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertPmAttachments' )
+ ),
+ 'convertPmAttachments' => array(
+ 'table' => 'exp_message_attachments',
+ 'where' => NULL,
+ ),
+ 'convertRanks' => array(
+ 'table' => 'exp_forum_ranks',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Can we convert settings?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return FALSE;
+ }
+
+ /**
+ * List of Conversion Methods that require more information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers',
+ 'convertPrivateMessages'
+ );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return nl2br( $post );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch ( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'signature_attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ee_sig_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'exp_member_fields' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['m_field_id']}"] = $field['m_field_label'];
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['m_field_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['m_field_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'exp_member_groups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['group_id']}"] = $group['group_title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['group_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['group_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* We can only retain one type of photo */
+ $return['convertMembers']['photo_type'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'avatars',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'avatars' => \IPS\Member::loggedIn()->language()->addToStack( 'avatars' ), 'profile_photos' => \IPS\Member::loggedIn()->language()->addToStack( 'profile_photos' ) ) ),
+ 'field_hint' => NULL,
+ );
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['photo_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'photo_location_nodb_desc' );
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ee_avatar_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ foreach( array( 'url', 'location', 'occupation', 'bio' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ case 'convertPrivateMessages':
+ $return['convertPrivateMessages'] = array(
+ 'pm_attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ee_pm_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish
+ *
+ * @return array
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ /* Attachments */
+ \IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'member_id' );
+
+ $it = $this->fetch( 'exp_members', 'member_id', array( "sig_img_filename<>''" ) );
+
+ foreach( $it AS $row )
+ {
+ $map = array(
+ 'id1' => $row['member_id'],
+ 'id1_type' => 'core_members',
+ 'id1_from_parent' => FALSE,
+ 'location_key' => 'core_Signatures'
+ );
+
+ $ext = explode( '.', $row['sig_img_filename'] );
+ $ext = array_pop( $ext );
+
+ $info = array(
+ 'attach_id' => $row['member_id'],
+ 'attach_file' => $row['sig_img_filename'],
+ 'attach_member_id' => $row['member_id'],
+ 'attach_ext' => $ext
+ );
+
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['signature_attach_location'], '/' ) . '/' . $row['sig_img_filename'];
+
+ $attachId = $libraryClass->convertAttachment( $info, $map, $path );
+
+ /* Update Post if we can */
+ try
+ {
+ if ( $attachId !== FALSE )
+ {
+ $ourAttachment = \IPS\File::get( 'core_Signature', \IPS\Db::i()->select( 'attach_location', 'core_attachments', array( 'attach_id=?', $attachId ) )->first() );
+
+ $memberId = $this->app->getLink( $row['member_id'], 'core_members' );
+
+ $signature = \IPS\Db::i()->select( 'signature', 'core_members', array( "member_id=?", $memberId ) )->first();
+
+ $signature .= '
';
+
+ \IPS\Db::i()->update( 'core_members', array( 'signature' => $signature ), array( "member_id=?", $memberId ) );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \OutOfRangeException $e ) {}
+
+ $libraryClass->setLastKeyValue( $row['member_id'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'exp_member_fields', 'm_field_id' ) AS $row )
+ {
+ $info = array();
+ $info['pf_id'] = $row['m_field_id'];
+ $merge = $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['m_field_id']}"] != 'none' ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['m_field_id']}"] : NULL;
+ $info['pf_type'] = ucwords( $row['m_field_type'] );
+ $info['pf_name'] = $row['m_field_label'];
+ $info['pf_desc'] = $row['m_field_description'];
+ $info['pf_content'] = ( !in_array( $row['m_field_type'], array( 'textbox', 'textarea' ) ) ) ? explode( "\r", $row['field_choices'] ) : NULL;
+ $info['pf_not_null'] = $row['m_field_required'] == 'y' ? 1 : 0;
+ $info['pf_member_hide'] = $row['m_field_public'] == 'n' ? 1 : 0;
+ $info['pf_max_input'] = $row['m_field_maxl'];
+ $info['pf_member_edit'] = ( in_array( $row['user_editable'], array( 'yes', 'once' ) ) ) ? 0 : 1;
+ $info['pf_position'] = $row['m_field_order'];
+ $info['pf_show_on_reg'] = $row['m_field_reg'] == 'y' ? 1 : 0;
+ $info['pf_input_format'] = NULL;
+ $info['pf_multiple'] = 0;
+
+ $libraryClass->convertProfileField( $info, $merge );
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'group_id' );
+
+ foreach( $this->fetch( 'exp_member_groups', 'group_id' ) AS $row )
+ {
+ /* Basic info */
+ $info = array(
+ 'g_id' => $row['group_id'],
+ 'g_name' => $row['group_title'],
+ 'g_view_board' => ( $row['can_view_online_system'] == 'y' ) ? 1 : 0,
+ 'g_use_pm' => ( $row['can_send_private_messages'] == 'y' ) ? 1 : 0,
+ 'g_max_messages' => $row['prv_msg_storage_limit'],
+ 'g_pm_perday' => $row['prv_msg_send_limit'],
+ 'g_search_flood' => $row['search_flood_control']
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['group_id']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['group_id']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['group_id'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'exp_members.member_id' );
+
+ $it = $this->fetch( 'exp_members', 'exp_members.member_id' );
+
+ foreach( $it AS $row )
+ {
+ /* Main Member Information */
+ $info = array(
+ 'member_id' => $row['member_id'],
+ 'name' => $row['screen_name'],
+ 'email' => $row['email'],
+ 'conv_password' => $row['password'],
+ 'member_group_id' => $row['group_id'],
+ 'joined' => $row['join_date'],
+ 'ip_address' => $row['ip_address'],
+ 'bday_day' => $row['bday_d'],
+ 'bday_month' => $row['bday_m'],
+ 'bday_year' => $row['bday_y'],
+ 'msg_count_total' => $row['private_messages'],
+ 'last_visit' => $row['last_visit'],
+ 'last_activity' => $row['last_activity'],
+ 'signature' => $row['signature'],
+ 'members_bitoptions' => array(
+ 'view_sigs' => ( $row['display_signatures'] == 'y' ) ? 1 : 0
+ ),
+ 'pp_setting_count_comments' => 1,
+ 'allow_admin_mails' => ( $row['accept_admin_email'] == 'y' ) ? 1 : 0,
+ 'member_posts' => $row['total_forum_posts'],
+ );
+
+ /* Profile Fields */
+ try
+ {
+ $profileFields = $this->db->select( '*', 'exp_member_data', array( "member_id=?", $row['member_id'] ) )->first();
+
+ unset( $profileFields['member_id'] );
+
+ /* Basic fields - we only need ID => Value, the library will handle the rest */
+ foreach( $profileFields AS $key => $value )
+ {
+ $profileFields[ str_replace( 'm_field_id_', '', $key ) ] = $value;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $profileFields = array();
+ }
+
+ /* Pseudo Fields */
+ foreach( array( 'url', 'location', 'occupation', 'bio' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 500,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $profileFields[ $pseudo ] = $row[ $pseudo ];
+ }
+
+ /* Profile Photos */
+ if ( $this->app->_session['more_info']['convertMembers']['photo_type'] == 'profile_photos' )
+ {
+ $filename = $row['photo_filename'];
+ }
+ else
+ {
+ $filename = $row['avatar_filename'];
+ }
+
+ $libraryClass->convertMember( $info, $profileFields, $filename, rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' ) );
+
+ $libraryClass->setLastKeyValue( $row['member_id'] );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'rank_id' );
+
+ foreach( $this->fetch( 'exp_forum_ranks', 'rank_id' ) AS $row )
+ {
+ $libraryClass->convertRank( array(
+ 'id' => $row['rank_id'],
+ 'title' => $row['rank_title'],
+ 'posts' => $row['rank_min_posts'],
+ 'pips' => $row['rank_stars']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['rank_id'] );
+ }
+ }
+
+ /**
+ * Convert PMs
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'message_id' );
+
+ foreach( $this->fetch( 'exp_message_data', 'message_id' ) AS $row )
+ {
+ /* Reset map array */
+ $maps = array();
+
+ $topic = array(
+ 'mt_id' => $row['message_id'],
+ 'mt_date' => $row['message_date'],
+ 'mt_title' => $row['message_subject'],
+ 'mt_starter_id' => $row['sender_id'],
+ 'mt_start_time' => $row['message_date'],
+ 'mt_last_post_time' => $row['message_date'],
+ 'mt_to_count' => $row['total_recipients'],
+ );
+
+ /* Author Map */
+ $maps[ $row['sender_id'] ] = array(
+ 'map_user_id' => $row['sender_id'],
+ 'map_read_time' => $row['message_date'],
+ 'map_user_active' => 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => 0,
+ 'map_is_starter' => 1,
+ 'map_last_topic_reply' => $row['message_date']
+ );
+
+ foreach( $this->db->select( '*', 'exp_message_copies', array( 'message_id=?', $row['message_id'] ) ) AS $map )
+ {
+ $maps[ $map['recipient_id'] ] = array(
+ 'map_user_id' => $map['recipient_id'],
+ 'map_read_time' => $map['message_time_read'],
+ 'map_user_active' => 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => ( $map['message_read'] == 'n' ) ? 1 : 0,
+ 'map_is_starter' => ( $map['recipient_id'] == $row['sender_id'] ) ? 1 : 0,
+ 'map_last_topic_reply' => $row['message_date']
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['message_id'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'message_id' );
+
+ foreach( $this->fetch( 'exp_message_data', 'message_id' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['message_id'],
+ 'msg_topic_id' => $row['message_id'],
+ 'msg_date' => $row['message_date'],
+ 'msg_post' => $row['message_body'],
+ 'msg_author_id' => $row['sender_id'],
+ 'msg_is_first_post' => 1,
+ 'msg_ip_address' => '127.0.0.1',
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['message_id'] );
+ }
+ }
+
+ /**
+ * Convert PM attachments
+ *
+ * @return void
+ */
+ public function convertPmAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'attachment_id' );
+
+ $it = $this->fetch( 'exp_message_attachments', 'attachment_id' );
+
+ foreach( $it AS $row )
+ {
+ $map = array(
+ 'id1' => $row['message_id'],
+ 'id1_type' => 'core_message_topics',
+ 'id1_from_parent' => FALSE,
+ 'id2' => $row['message_id'],
+ 'id2_type' => 'core_message_posts',
+ 'id2_from_parent' => FALSE
+ );
+
+ $info = array(
+ 'attach_id' => $row['attachment_id'],
+ 'attach_file' => $row['attachment_name'],
+ 'attach_date' => $row['attachment_date'],
+ 'attach_member_id' => $row['sender_id'],
+ 'attach_ext' => \strtolower( trim( $row['attachment_extension'], '.' ) ),
+ 'attach_filesize' => ( $row['attachment_size'] * 1000 ), //convert to kb
+ );
+
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['pm_attach_location'], '/' ) . '/' . $row['attachment_hash'] . $row['attachment_extension'];
+
+ $attachId = $libraryClass->convertAttachment( $info, $map, $path );
+
+ /* Update Post if we can */
+ try
+ {
+ if ( $attachId !== FALSE )
+ {
+ $ourAttachment = \IPS\File::get( 'core_Attachment', \IPS\Db::i()->select( 'attach_location', 'core_attachments', array( 'attach_id=?', $attachId ) )->first() );
+
+ $pmId = $this->app->getLink( $row['message_id'], 'core_message_posts' );
+
+ $message = \IPS\Db::i()->select( 'msg_post', 'core_message_posts', array( "msg_id=?", $pmId ) )->first();
+
+ $message .= '
';
+
+ \IPS\Db::i()->update( 'core_message_posts', array( 'msg_post' => $message ), array( "msg_id=?", $pmId ) );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \OutOfRangeException $e ) {}
+
+ $libraryClass->setLastKeyValue( $row['attachment_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/member/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Joomla.php b/applications/convert/sources/Software/Core/Joomla.php
new file mode 100644
index 00000000..3616be86
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Joomla.php
@@ -0,0 +1,223 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Joomla extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Joomla";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "joomla";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGroups' => array(
+ 'table' => 'usergroups',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'users',
+ 'where' => NULL
+ ),
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGroups'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Method name
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'usergroups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['id']}"] = $group['title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches" );
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'usergroups', 'id' ) as $row )
+ {
+ /* Basic info */
+ $info = array(
+ 'g_id' => $row['id'],
+ 'g_name' => $row['title'],
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['id']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['id']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'users', 'id' ) as $row )
+ {
+ /* Basic info */
+ $info = array(
+ 'member_id' => $row['id'],
+ 'name' => $row['username'],
+ 'email' => $row['email'],
+ 'joined' => strtotime( $row['registerDate'] ),
+ 'mgroup_others' => array(),
+ 'temp_ban' => $row['block'] ? -1 : 0
+ );
+
+ /* Figure out password */
+ $pass = explode ( ':', $row['password'] );
+ $info['conv_password'] = $pass[0];
+ $info['conv_password_extra'] = $pass[1];
+
+ /* Figure out groups */
+ $libraryClass::$usingKeys = FALSE;
+ foreach( $this->fetch( 'user_usergroup_map', 'group_id', array( 'user_id=?', $row['id'] ), 'group_id' ) as $groupId )
+ {
+ /* First one we find we'll set as primary - Joomla does not make a distinction */
+ if( !isset( $info['member_group_id'] ) )
+ {
+ $info['member_group_id'] = $groupId;
+ }
+ else
+ {
+ $info['mgroup_others'][] = $groupId;
+ }
+ }
+ $libraryClass::$usingKeys = TRUE;
+
+ $libraryClass->convertMember( $info );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Mybb.php b/applications/convert/sources/Software/Core/Mybb.php
new file mode 100644
index 00000000..332fd10c
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Mybb.php
@@ -0,0 +1,1072 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Mybb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "MyBB 1.8.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "mybb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertBanfilters' => array(
+ 'table' => 'banfilters',
+ 'where' => NULL
+ ),
+ 'convertEmoticons' => array(
+ 'table' => 'smilies',
+ 'where' => NULL,
+ ),
+ 'convertWarnActions' => array(
+ 'table' => 'warninglevels',
+ 'where' => NULL
+ ),
+ 'convertWarnReasons' => array(
+ 'table' => 'warningtypes',
+ 'where' => NULL
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'profilefields',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'usergroups',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'users', # note convert ignored users during this step, rather than individually
+ 'where' => NULL
+ ),
+ 'convertAnnouncements' => array(
+ 'table' => 'announcements',
+ 'where' => NULL
+ ),
+ 'convertRanks' => array(
+ 'table' => 'usertitles',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'privatemessages',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'privatemessages',
+ 'where' => NULL,
+ ),
+ 'convertProfanityFilters' => array(
+ 'table' => 'badwords',
+ 'where' => NULL
+ ),
+ 'convertQuestionAndAnswers' => array(
+ 'table' => 'questions',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ \IPS\Member::loggedIn()->language()->words['emoticon_path'] = \IPS\Member::loggedIn()->language()->addToStack( 'source_path', FALSE, array( 'sprintf' => array( 'MyBB' ) ) );
+ $return['convertEmoticons']['emoticon_path'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ $return['convertEmoticons']['keep_existing_emoticons'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ );
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profilefields' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['fid']}"] = $field['name'];
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['fid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['fid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'usergroups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['gid']}"] = $group['title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['gid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['gid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL
+ );
+ }
+
+ \IPS\Member::loggedIn()->language()->words['icon_path'] = \IPS\Member::loggedIn()->language()->addToStack( 'source_path', FALSE, array( 'sprintf' => array( 'MyBB' ) ) );
+ $return['convertGroups']['icon_path'] = array(
+ 'field_class' => "IPS\\Helpers\\Form\\Text",
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_groupicon_hint'),
+ 'field_validation' => function( $value ) { if ( !empty( $value ) AND !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['photo_location'] = \IPS\Member::loggedIn()->language()->addToStack( 'source_path', FALSE, array( 'sprintf' => array( 'MyBB' ) ) );
+ \IPS\Member::loggedIn()->language()->words['photo_location_desc'] = '';
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ foreach( array( 'website', 'icq', 'aim', 'yahoo', 'skype', 'google' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_announcements', 'extension' => 'core_Announcement' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_announce_rebuild" );
+ }
+
+ /**
+ * Convert announcements
+ *
+ * @return void
+ */
+ public function convertAnnouncements()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'aid' );
+
+ foreach ( $this->fetch( 'announcements', 'aid' ) AS $row )
+ {
+ $libraryClass->convertAnnouncement( array(
+ 'announce_id' => $row['aid'],
+ 'announce_title' => $row['subject'],
+ 'announce_content' => $row['message'],
+ 'announce_member_id' => $row['uid'],
+ 'announce_start' => $row['startdate'],
+ 'announce_end' => $row['enddate'],
+ 'announce_active' => ( $row['enddate'] == 0 OR $row['enddate'] > time() ) ? 1 : 0,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['aid'] );
+ }
+ }
+
+ /**
+ * Convert ban filters
+ *
+ * @return void
+ */
+ public function convertBanfilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'fid' );
+
+ foreach( $this->fetch( 'banfilters', 'fid' ) AS $row )
+ {
+ switch( $row['type'] )
+ {
+ case 1:
+ $type = 'ip';
+ break;
+
+ case 2:
+ $type = 'name';
+ break;
+
+ case 3:
+ $type = 'email';
+ break;
+ }
+
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row['fid'],
+ 'ban_type' => $type,
+ 'ban_content' => $row['filter'],
+ 'ban_date' => $row['dateline']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['fid'] );
+ }
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'sid' );
+
+ foreach( $this->fetch( 'smilies', 'sid' ) AS $row )
+ {
+ $set = array(
+ 'set' => md5( 'Converted' ),
+ 'title' => 'Converted',
+ 'position' => 1,
+ );
+
+ $filepath = explode( '/', $row['image'] );
+ $filename = array_pop( $filepath );
+ $path = rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . '/' . implode( '/', $filepath );
+
+ $typed = explode( "\n", $row['find'] );
+ $typed = array_shift( $typed );
+
+ $info = array(
+ 'id' => $row['sid'],
+ 'typed' => $typed,
+ 'filename' => $filename,
+ 'clickable' => $row['showclickable'],
+ 'emo_position' => $row['disporder']
+ );
+
+ $libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $path );
+
+ $libraryClass->setLastKeyValue( $row['sid'] );
+ }
+ }
+
+ /**
+ * Convert warn actions
+ *
+ * @return void
+ */
+ public function convertWarnActions()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'lid' );
+
+ foreach( $this->fetch( 'warninglevels', 'lid' ) AS $row )
+ {
+ $action = \unserialize( $row['action'] );
+
+ $mq = 0;
+ $mq_unit = 'h';
+ $rpa = 0;
+ $rpa_unit = 'h';
+ $suspend = 0;
+ $suspend_unit = 'h';
+ switch( $action['type'] )
+ {
+ // Banned
+ case 1:
+ if ( $action['length'] >= 86400 )
+ {
+ $suspend_unit = 'd';
+ $suspend = floor( $action['length'] / 86400 );
+ }
+ else
+ {
+ $suspend_unit = 'h';
+ $suspend = floor( $action['length'] / 3600 );
+ }
+ break;
+
+ // Restrict Posting
+ case 2:
+ if ( $action['length'] >= 86400 )
+ {
+ $rpa_unit = 'd';
+ $rpa = floor( $action['length'] / 86400 );
+ }
+ else
+ {
+ $rpa_unit = 'h';
+ $rpa = floor( $action['length'] / 3600 );
+ }
+ break;
+
+ // Moderate Posting
+ case 3:
+ if ( $action['length'] >= 86400 )
+ {
+ $mq_unit = 'd';
+ $mq = floor( $action['length'] / 86400 );
+ }
+ else
+ {
+ $mq_unit = 'h';
+ $mq = floor( $action['length'] / 3600 );
+ }
+ break;
+ }
+
+ $libraryClass->convertWarnAction( array(
+ 'wa_id' => $row['lid'],
+ 'wa_points' => $row['percentage'],
+ 'wa_mq' => $mq,
+ 'wa_mq_unit' => $mq_unit,
+ 'wa_rpa' => $rpa,
+ 'wa_rpa_unit' => $rpa_unit,
+ 'wa_suspend' => $suspend,
+ 'wa_suspend_unit' => $suspend_unit,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['lid'] );
+ }
+ }
+
+ /**
+ * Convert warn reasons
+ *
+ * @return void
+ */
+ public function convertWarnReasons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'tid' );
+
+ foreach( $this->fetch( 'warningtypes', 'tid' ) AS $row )
+ {
+ $libraryClass->convertWarnReason( array(
+ 'wr_id' => $row['tid'],
+ 'wr_name' => $row['title'],
+ 'wr_points' => $row['points'],
+ 'wr_remove' => floor( $row['expirationtime'] / 3600 ),
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['tid'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'fid' );
+
+ foreach( $this->fetch( 'profilefields', 'fid' ) AS $row )
+ {
+ /* MyBB stores the type and the options in the same column. */
+ $typeAndOptions = explode( "\n", $row['type'] );
+ $theirType = array_shift( $typeAndOptions );
+ $ourType = $this->_fieldMap( $theirType );
+ $options = $typeAndOptions;
+
+ $info = array(
+ 'pf_id' => $row['fid'],
+ 'pf_type' => $ourType,
+ 'pf_name' => $row['name'],
+ 'pf_desc' => $row['description'],
+ 'pf_content' => $options,
+ 'pf_not_null' => $row['required'],
+ 'pf_member_hide' => $row['profile'],
+ 'pf_max_input' => $row['maxlength'],
+ 'pf_position' => $row['disporder'],
+ 'pf_show_on_reg' => $row['registration'],
+ 'pf_input_format' => '/' . $row['regex'] . '/i',
+ 'pf_admin_only' => ( $row['editableby'] == -1 ) ? 0 : 1, # MyBB allows you to define which groups can edit, so if this value is anything other than -1, assume it's admin only
+ 'pf_multiple' => ( $theirType == 'multiselect' ) ? 1 : 0
+ );
+
+ $merge = $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['fid']}"] != 'none' ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['fid']}"] : NULL;
+
+ $libraryClass->convertProfileField( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['fid'] );
+ }
+ }
+
+ /**
+ * Maps a MyBB Profile Field type to IPS
+ *
+ * @param string MyBB Type
+ * @return string IPS Type
+ */
+ protected function _fieldMap( $type )
+ {
+ switch( $type )
+ {
+ case 'text':
+ return 'Text';
+ break;
+
+ case 'textarea':
+ return "TextArea";
+ break;
+
+ case 'select':
+ case 'multiselect':
+ return 'Select';
+ break;
+
+ case 'checkbox':
+ return 'CheckboxSet';
+ break;
+
+ case 'radio':
+ return 'Radio';
+ break;
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'gid' );
+
+ foreach( $this->fetch( 'usergroups', 'gid' ) AS $row )
+ {
+ /* Are we trying to fetch icons? */
+ $icon_path = $this->app->_session['more_info']['convertGroups']['icon_path'];
+
+ /* Work out promotions */
+ $promotion_unit = 0;
+ $promotion_data = NULL;
+ try
+ {
+ /* We only support one type of promotion, so just fetch the first one we can find */
+ $promotion = $this->db->select( '*', 'promotions', array( "originalusergroup=? AND usergrouptype=? AND ( requirements=? OR requirements=? )", $row['gid'], 'primary', 'postcount', 'timeregistered' ) )->first();
+
+ switch( $promotion['requirements'] )
+ {
+ case 'postcount':
+ if ( !in_array( $promotion['posttype'], array( '>', '>=', '=' ) ) )
+ {
+ /* Bubble up */
+ throw new \UnderflowException;
+ }
+ $promotion_unit = 0;
+ $promotion_data = array( $promotion['newusergroup'], $promotion['posts'] );
+ break;
+
+ case 'timeregistered':
+ if ( !in_array( $promotion['registeredtype'], array( '>', '>=', '=' ) ) )
+ {
+ throw new \UnderflowException;
+ }
+ $promotion_unit = 0;
+ $promotion_data = array( $promotion['newusergroup'], $promotion['registered'] );
+ break;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Username Styles */
+ $style = explode( '{username}', $row['namestyle'] );
+ $prefix = isset( $style[0] ) ? $style[0] : '';
+ $suffix = isset( $style[1] ) ? $style[1] : '';
+
+ $info = array(
+ 'g_id' => $row['gid'],
+ 'g_name' => $row['title'],
+ 'g_icon' => ( $icon_path AND $row['image'] ) ? $row['image'] : NULL,
+ 'g_promotion' => $promotion_data ?: NULL,
+ 'g_bitoptions' => array(
+ 'gbw_promote_unit_type' => $promotion_unit,
+ ),
+ 'g_view_board' => $row['canview'],
+ 'g_mem_info' => $row['canviewprofiles'],
+ 'g_use_search' => $row['cansearch'],
+ 'g_edit_profile' => $row['canusercp'],
+ 'g_edit_posts' => $row['caneditposts'],
+ 'g_use_pm' => $row['cansendpms'],
+ 'g_pm_flood_mins' => $row['emailfloodtime'],
+ 'g_post_polls' => $row['canpostpolls'],
+ 'g_vote_polls' => $row['canvotepolls'],
+ 'g_delete_own_posts' => $row['candeleteposts'],
+ 'g_is_supmod' => $row['issupermod'],
+ 'g_access_cp' => $row['cancp'],
+ 'g_access_offline' => $row['canviewboardclosed'],
+ 'g_max_messages' => $row['pmquota'],
+ 'g_attach_max' => ( $row['attachquota'] == 0 ) ? -1 : $row['attachquota'],
+ 'prefix' => $prefix,
+ 'suffix' => $suffix,
+ 'g_max_mass_pm' => $row['maxpmrecipients'],
+ 'g_rep_max_positive' => $row['maxreputationsday'],
+ 'g_rep_max_negative' => $row['maxreputationsday'],
+ );
+
+ $merge = $this->app->_session['more_info']['convertGroups']["map_group_{$row['gid']}"] != 'none' ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['gid']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge, ( $icon_path ) ? rtrim( $icon_path, '/' ) . '/' . $row['image'] : NULL );
+
+ $libraryClass->setLastKeyValue( $row['gid'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'uid' );
+
+ foreach( $this->fetch( 'users', 'uid' ) AS $row )
+ {
+ /* Last warning */
+ $last_warn = 0;
+ try
+ {
+ $last_warn = $this->db->select( 'dateline', 'warnings', array( "uid=?", $row['uid'] ), "dateline DESC" )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Restrict posting */
+ $restrict_post = 0;
+ if ( $row['suspendposting'] )
+ {
+ if ( $row['suspensiontime'] > 0 )
+ {
+ $restrict_post = $row['suspensiontime'];
+ }
+ else
+ {
+ $restrict_post = -1;
+ }
+ }
+
+ /* Birthday */
+ $bday_day = $bday_month = $bday_year = NULL;
+ $birthday = explode( '-', $row['birthday'] );
+
+ if ( isset( $birthday[0] ) )
+ {
+ $bday_day = $birthday[0];
+ }
+
+ if ( isset( $birthday[1] ) )
+ {
+ $bday_month = $birthday[1];
+ }
+
+ if ( isset( $birthday[2] ) )
+ {
+ $bday_year = $birthday[2];
+ }
+
+ /* Moderate Posts */
+ $mod_posts = 0;
+ if ( $row['moderateposts'] )
+ {
+ if ( $row['moderationtime'] > 0 )
+ {
+ $mod_posts = $row['moderationtime'];
+ }
+ else
+ {
+ $mod_posts = -1;
+ }
+ }
+
+ /* Subscription */
+ switch( $row['subscriptionmethod'] )
+ {
+ case 0:
+ $auto_track = array( 'content' => 0, 'comments' => 0, 'method' => 'none' );
+ break;
+
+ case 1:
+ $auto_track = array( 'content' => 1, 'comments' => 1, 'method' => 'none' );
+ break;
+
+ case 2:
+ case 3:
+ $auto_track = array( 'content' => 1, 'comments' => 1, 'method' => 'immediate' );
+ break;
+ }
+
+ /* Banned */
+ $temp_ban = 0;
+ try
+ {
+ $temp_ban = $this->db->select( 'lifted', 'banned', array( "uid=?", $row['uid'] ) )->first();
+
+ if ( $temp_ban == 0 )
+ {
+ $temp_ban = -1;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'member_id' => $row['uid'],
+ 'name' => $row['username'],
+ 'email' => $row['email'],
+ 'password' => $row['password'],
+ 'password_extra' => $row['salt'],
+ 'member_group_id' => $row['usergroup'],
+ 'joined' => $row['regdate'],
+ 'ip_address' => $row['regip'],
+ 'warn_level' => $row['warningpoints'],
+ 'warn_lastwarn' => $last_warn,
+ 'restrict_post' => $restrict_post,
+ 'bday_day' => str_pad( $bday_day, 2, '0', STR_PAD_LEFT ),
+ 'bday_month' => str_pad( $bday_month, 2, '0', STR_PAD_LEFT ),
+ 'bday_year' => $bday_year ? intval( $bday_year ) : NULL,
+ 'msg_count_new' => $row['unreadpms'],
+ 'msg_count_total' => $row['totalpms'],
+ 'msg_show_notification' => $row['pmnotify'],
+ 'last_visit' => $row['lastvisit'],
+ 'last_activity' => $row['lastactive'],
+ 'mod_posts' => $mod_posts,
+ 'auto_track' => $auto_track,
+ 'temp_ban' => $temp_ban,
+ 'mgroup_others' => $row['additionalgroups'],
+ 'members_bitoptions' => array(
+ 'view_sigs' => $row['showsigs'],
+ ),
+ 'members_bitoptions2' => array(
+ 'show_pm_popup' => $row['pmnotify'],
+ ),
+ 'pp_reputation_points' => $row['reputation'],
+ 'signature' => $row['signature'],
+ 'timezone' => $row['timezone'],
+ 'allow_admin_mails' => $row['allownotices'],
+ 'members_disable_pm' => ( $row['receivepms'] ) ? 0 : 1,
+ 'member_title' => $row['usertitle'],
+ 'member_posts' => $row['postnum'],
+ 'member_last_post' => $row['lastpost'],
+ );
+
+ $pfields = array();
+ foreach( $this->db->select( '*', 'userfields', array( "ufid=?", $row['uid'] ) ) AS $key => $field )
+ {
+ $key = str_replace( 'fid', '', $key );
+ $pfields[ $key ] = $field;
+ }
+
+ foreach( array( 'website', 'icq', 'aim', 'yahoo', 'skype', 'google' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $pfields[$pseudo] = $row[$pseudo];
+ }
+
+ /* Photo */
+ $filename = NULL;
+ $filepath = NULL;
+ switch( $row['avatartype'] )
+ {
+ case 'gravatar':
+ $info['pp_gravatar'] = $row['email'];
+ $info['pp_photo_type'] = 'gravatar';
+ break;
+
+ case 'upload':
+ $info['pp_photo_type'] = 'custom';
+
+ $avatar = trim( '.', $row['avatar'] );
+ $filepath = explode( '/', $avatar );
+ $filename = array_pop( $filepath );
+ $filepath = rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' ) . implode( '/', $filepath );
+ break;
+ }
+
+ $libraryClass->convertMember( $info, $pfields, $filename, $filepath );
+
+ /* Followers */
+ foreach( explode( ',', $row['buddylist'] ) AS $buddy )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'members',
+ 'follow_rel_id' => $buddy,
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $row['uid'],
+ ) );
+ }
+
+ /* Ignore List */
+ foreach( explode( ',', $row['ignorelist'] ) AS $ignore )
+ {
+ $libraryClass->convertIgnoredUser( array(
+ 'ignore_id' => $row['uid'] . '-' . $ignore,
+ 'ignore_owner_id' => $row['uid'],
+ 'ignore_ignore_id' => $ignore
+ ) );
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'warnings', array( "uid=? AND pid=?", $row['uid'], 0 ) ) AS $warning )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warning['wid'],
+ 'wl_member' => $warning['uid'],
+ 'wl_moderator' => $warning['issuedby'],
+ 'wl_date' => $warning['dateline'],
+ 'wl_reason' => $warning['tid'],
+ 'wl_points' => $warning['points'],
+ 'wl_note_mods' => $warning['notes'],
+ 'wl_expire_date' => $warning['expires'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['uid'] );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'utid' );
+
+ foreach( $this->fetch( 'usertitles', 'utid' ) AS $row )
+ {
+ $libraryClass->convertRank( array(
+ 'id' => $row['utid'],
+ 'title' => $row['title'],
+ 'posts' => $row['posts'],
+ 'pips' => $row['stars']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['utid'] );
+ }
+ }
+
+ /**
+ * Convert PMs
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pmid' );
+
+ foreach( $this->fetch( 'privatemessages', 'pmid' ) AS $row )
+ {
+ /* My hands are kind of tied here... every individual message is stored separately, rather than grouped into conversations. In this instance, we'll bring over each individual message as it's own topic for archival purposes. */
+ $recipients = \unserialize( $row['recipients'] );
+
+ $topic = array(
+ 'mt_id' => $row['pmid'],
+ 'mt_date' => $row['dateline'],
+ 'mt_title' => $row['subject'],
+ 'mt_starter_id' => $row['uid'],
+ 'mt_start_time' => $row['dateline'],
+ 'mt_last_post_time' => $row['dateline'],
+ 'mt_to_count' => 1,
+ );
+
+ $maps = array();
+ $maps[$row['uid']] = array(
+ 'map_user_id' => $row['uid'],
+ 'map_read_time' => $row['readtime'],
+ );
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['pmid'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pmid' );
+
+ foreach( $this->fetch( 'privatemessages', 'pmid' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['pmid'],
+ 'msg_topic_id' => $row['pmid'],
+ 'msg_date' => $row['dateline'],
+ 'msg_post' => $row['message'],
+ 'msg_author_id' => $row['uid'],
+ 'msg_ip_address' => $row['ipaddress'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['pmid'] );
+ }
+ }
+
+ /**
+ * Convert profanity filters
+ *
+ * @return void
+ */
+ public function convertProfanityFilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'bid' );
+
+ foreach( $this->fetch( 'badwords', 'bid' ) AS $row )
+ {
+ $libraryClass->convertProfanityFilter( array(
+ 'wid' => $row['bid'],
+ 'type' => $row['badword'],
+ 'swop' => $row['replacement'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['bid'] );
+ }
+ }
+
+ /**
+ * Convert questions and answers
+ *
+ * @return void
+ */
+ public function convertQuestionAndAnswers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'qid' );
+
+ foreach( $this->fetch( 'questions', 'qid' ) AS $row )
+ {
+ $answers = explode( "\n", $row['answer'] );
+
+ $libraryClass->convertQuestionAndAnswer( array(
+ 'qa_id' => $row['qid'],
+ 'qa_question' => $row['question'],
+ ), $answers );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'member.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()-uid, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Photopost.php b/applications/convert/sources/Software/Core/Photopost.php
new file mode 100644
index 00000000..fe5116aa
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Photopost.php
@@ -0,0 +1,86 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 6 December 2016
+ * @note Only redirect scripts are supported right now
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Photopost extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Photopost";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "photopost";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return NULL
+ */
+ public static function canConvert()
+ {
+ return NULL;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'member.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->cat, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Phpbb.php b/applications/convert/sources/Software/Core/Phpbb.php
new file mode 100644
index 00000000..1135a333
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Phpbb.php
@@ -0,0 +1,1024 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Phpbb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "phpBB 3.1";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "phpbb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertEmoticons' => array(
+ 'table' => 'smilies',
+ 'where' => NULL
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'profile_fields',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'groups',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'users',
+ 'where' => array( "user_type<>?", 2 )
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'zebra',
+ 'where' => array( "foe=?", 1 )
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'privmsgs',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'privmsgs',
+ 'where' => NULL,
+ ),
+ 'convertRanks' => array(
+ 'table' => 'ranks',
+ 'where' => array( "rank_special!=?", 1 ),
+ ),
+ 'convertProfanityFilters' => array(
+ 'table' => 'words',
+ 'where' => NULL
+ ),
+ 'convertBanfilters' => array(
+ 'table' => 'banfilters',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertBanfilters2' ),
+ ),
+ 'convertBanfilters2' => array(
+ 'table' => 'disallow',
+ 'where' => NULL,
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'banfilters':
+ case 'disallow':
+ $count = 0;
+ $count += $this->db->select( 'COUNT(*)', 'banlist', array( "ban_userid=?", 0 ) )->first();
+ $count += $this->db->select( 'COUNT(*)', 'disallow' )->first();
+ return $count;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_phpbb_preconvert';
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers',
+ 'convertRanks',
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ $return['convertEmoticons'] = array(
+ 'emoticon_path' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_emoticons'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ 'keep_existing_emoticons' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ )
+ );
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profile_fields' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}"] = $this->db->select( 'lang_name', 'profile_lang', array( "field_id=?", $field['field_id'] ) )->first();
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['field_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'groups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['group_id']}"] = $group['group_name'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['group_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['group_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* We can only retain one type of photo */
+ $return['convertMembers']['photo_hash'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_randstring'),
+ );
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['photo_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'photo_location_nodb_desc' );
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_avatars'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ $return['convertMembers']['gallery_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_avatargallery'),
+ 'field_validation' => function( $value ) { if ( $value AND !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ break;
+
+ case 'convertRanks':
+ $return['convertRanks']['rank_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_ranks_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild" );
+ }
+
+ /**
+ * Strip the bbcode UID
+ *
+ * @param string $post Post text
+ * @param string $uid BBCode uid
+ * @note PHPBB does bbcode like '[b:lsu27ljs]Bold text here[/b:lsu27ljs]' so we need to strip these uids to parse properly
+ * @return string
+ */
+ public static function stripUid( $post, $uid )
+ {
+ return str_replace( ":" . $uid . "]", "]", $post );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ /* Convert newlines to tags */
+ $post = nl2br($post);
+
+ /* Convert HTML entities back into actual entities */
+ $post = html_entity_decode($post, ENT_COMPAT | ENT_HTML401, "UTF-8");
+
+ /* Rework quotes so they'll match our parser style */
+ $post = str_replace( '][quote', "]\n[quote", $post );
+ $post = preg_replace( "#\[quote=(.+)\]#", "[quote name=$1]", $post );
+
+ /* Convert size tags */
+ $post = preg_replace_callback( '/\[size=(\d+)\]/', function( $match ) {
+ return '[size=' . ceil( ( $match[1] / 100 ) * 4 ) . ']';
+ }, $post );
+
+ /* Get rid of extra list markup we don't need */
+ $post = str_replace( array( "[/*]", "[/*:m]" ), "", $post );
+ $post = str_replace( array( "[/list:o]", "[/list:u]" ), "[/list]", $post );
+
+ /* Sort out emoticons */
+ $post = preg_replace("/ /", '$1', $post);
+
+ /* And sort out URLs */
+ $post = preg_replace("#]+?)?>([^\]]+?) #i", "[url=$1]$3[/url]", $post);
+
+ return $post;
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'smiley_id' );
+
+ foreach( $this->fetch( 'smilies', 'smiley_id' ) AS $row )
+ {
+ $libraryClass->convertEmoticon( array(
+ 'id' => $row['smiley_id'],
+ 'typed' => $row['code'],
+ 'filename' => $row['smiley_url'],
+ 'emo_position' => $row['smiley_order'],
+ 'width' => $row['smiley_width'],
+ 'height' => $row['smiley_height'],
+ ), array(
+ 'set' => md5( 'Converted' ),
+ 'title' => 'Converted',
+ 'position' => 1,
+ ), $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $this->app->_session['more_info']['convertEmoticons']['emoticon_path'] );
+
+ $libraryClass->setLastKeyValue( $row['smiley_id'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'field_id' );
+
+ foreach( $this->fetch( 'profile_fields', 'field_id' ) AS $row )
+ {
+ try
+ {
+ $name = $this->db->select( 'lang_name', 'profile_lang', array( "field_id=?", $row['field_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $name = $row['field_name'];
+ }
+
+ $field_type = explode( '.', $row['field_type'] );
+ $field_type = array_pop( $field_type );
+ $default = $row['field_default_value'];
+
+ switch( $field_type )
+ {
+ case 'bool':
+ $type = 'YesNo';
+ break;
+
+ case 'date':
+ $type = 'Date';
+ break;
+
+ case 'dropdown':
+ $type = 'Select';
+
+ $options = array();
+
+ foreach( $this->db->select( 'option_id, lang_value', 'profile_fields_lang', array( "field_id=?", $row['field_id'] ) )->setKeyField( 'option_id' )->setValueField( 'lang_value' ) AS $key => $value )
+ {
+ $options[$key] = $value;
+ }
+
+ $default = json_encode( $options );
+ break;
+
+ case 'googleplus':
+ case 'string':
+ $type = 'Text';
+ break;
+
+ case 'int':
+ $type = 'Number';
+ break;
+
+ case 'text':
+ $type = 'TextArea';
+ break;
+
+ case 'url':
+ $type = 'Url';
+ break;
+
+ default:
+ $type = 'Text';
+ break;
+ }
+
+ $info = array(
+ 'pf_id' => $row['field_id'],
+ 'pf_name' => $name,
+ 'pf_type' => $type,
+ 'pf_content' => $default,
+ 'pf_not_null' => $row['field_required'],
+ 'pf_member_hide' => $row['field_hide'],
+ 'pf_max_input' => ( $field_type == 'dropdown' ) ? NULL : $row['field_maxlen'], # ignore this for selects
+ 'pf_member_edit' => $row['field_active'],
+ 'pf_position' => $row['field_order'],
+ 'pf_show_on_reg' => $row['field_show_on_reg'],
+ 'pf_input_format' => $row['field_validation'] ? '/' . preg_quote( $row['field_validation'], '/' ) . '/i' : NULL,
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] : NULL;
+
+ $libraryClass->convertProfileField( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['field_id'] );
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'group_id' );
+
+ foreach( $this->fetch( 'groups', 'group_id' ) AS $row )
+ {
+ $prefix = '';
+ $suffix = '';
+
+ if ( $row['group_colour'] )
+ {
+ $prefix = "";
+ $suffix = " ";
+ }
+
+ $info = array(
+ 'g_id' => $row['group_id'],
+ 'g_name' => $row['group_name'],
+ 'g_use_pm' => $row['group_receive_pm'],
+ 'g_max_messages' => $row['group_message_limit'],
+ 'g_max_mass_pm' => $row['group_max_recipients'],
+ 'prefix' => $prefix,
+ 'suffix' => $suffix,
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['group_id']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['group_id']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'user_id' );
+
+ foreach( $this->fetch( 'users', 'user_id', array( "user_type<>?", 2 ) ) AS $row )
+ {
+ /* Work out birthday */
+ $bday_day = NULL;
+ $bday_month = NULL;
+ $bday_year = NULL;
+
+ if ( $row['user_birthday'] AND $row['user_birthday'] != " 0- 0- 0" )
+ {
+ list( $bday_day, $bday_month, $bday_year ) = explode( '-', $row['user_birthday'] );
+
+ if ( trim( $bday_day, ' -' ) == '0' )
+ {
+ $bday_day = NULL;
+ }
+
+ if ( trim( $bday_month, ' -' ) == '0' )
+ {
+ $bday_month = NULL;
+ }
+
+ if ( trim( $bday_year ) == '0' )
+ {
+ $bday_year = NULL;
+ }
+ }
+
+ /* Work out secondary groups */
+ $groups = array();
+ foreach( $this->db->select( 'group_id', 'user_group', array( "user_id=? AND group_id!=? AND user_pending=?", $row['user_id'], $row['group_id'], 0 ) ) AS $group )
+ {
+ $groups[] = $group;
+ }
+
+ /* Work out timezone - we don't really need to create an instance of \DateTimeZone here, however it helps check for invalid ones */
+ try
+ {
+ $timezone = new \DateTimeZone( $row['user_timezone'] );
+ }
+ catch( \Exception $e )
+ {
+ $timezone = 'UTC';
+ }
+
+ /* Work out banned stuff */
+ $temp_ban = 0;
+ try
+ {
+ $ban = $this->db->select( '*', 'banlist', array( "ban_userid=?", $row['user_id'] ) )->first();
+
+ if ( $ban['ban_end'] == 0 )
+ {
+ $temp_ban = -1;
+ }
+ else
+ {
+ $temp_ban = $ban['ban_end'];
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Array of basic data */
+ $info = array(
+ 'member_id' => $row['user_id'],
+ 'email' => $row['user_email'],
+ 'name' => $row['username'],
+ 'password' => $row['user_password'],
+ 'member_group_id' => $row['group_id'],
+ 'joined' => $row['user_regdate'],
+ 'ip_address' => $row['user_ip'],
+ 'warn_level' => $row['user_warnings'],
+ 'warn_lastwarn' => $row['user_last_warning'],
+ 'bday_day' => $bday_day,
+ 'bday_month' => $bday_month,
+ 'bday_year' => $bday_year,
+ 'msg_count_new' => $row['user_unread_privmsg'],
+ 'msg_count_total' => $this->db->select( 'COUNT(*)', 'privmsgs_to', array( "user_id=?", $row['user_id'] ) )->first(),
+ 'last_visit' => $row['user_lastvisit'],
+ 'last_activity' => $row['user_lastmark'],
+ 'mgroup_others' => $groups,
+ 'timezone' => $timezone,
+ 'allow_admin_mails' => ( $row['user_allow_massemail'] ) ? TRUE : FALSE,
+ 'members_disable_pm' => ( $row['user_allow_pm'] ) ? 0 : 1,
+ 'member_posts' => $row['user_posts'],
+ 'member_last_post' => $row['user_lastpost_time'],
+ 'signature' => static::stripUid( $row['user_sig'], $row['user_sig_bbcode_uid'] ),
+ );
+
+ /* Profile Photos */
+ $filepath = NULL;
+ $filename = NULL;
+
+ if ( $row['user_avatar_type'] )
+ {
+ /* Is it numeric? Apparently phpBB doesn't clean up after itself. */
+ if ( is_numeric( $row['user_avatar_type'] ) )
+ {
+ switch( $row['user_avatar_type'] )
+ {
+ case 1:
+ $row['user_avatar_type'] = 'avatar.driver.upload';
+ break;
+
+ case 2:
+ $row['user_avatar_type'] = 'avatar.driver.remote';
+ break;
+
+ case 3:
+ $row['user_avatar_type'] = 'avatar.driver.gallery';
+ break;
+ }
+ }
+
+ switch( $row['user_avatar_type'] )
+ {
+ case 'avatar.driver.upload':
+ $fileext = explode( '.', $row['user_avatar'] );
+ $fileext = array_pop( $fileext );
+ $filepath = $this->app->_session['more_info']['convertMembers']['photo_location'];
+ $filename = $this->app->_session['more_info']['convertMembers']['photo_hash'] . '_' . $row['user_id'] . '.' . $fileext;
+ break;
+
+ case 'avatar.driver.gravatar':
+ $info['pp_photo_type'] = 'gravatar';
+ $info['pp_gravatar'] = $row['user_avatar'];
+ break;
+
+ case 'avatar.driver.remote':
+ /* The library uses file_get_contents() so we can just pop the file name off and pass the URL directly */
+ $filebits = explode( "/", $row['user_avatar'] );
+ $filename = array_pop( $filebits );
+ $filepath = implode( '/', $filebits );
+ break;
+
+ case 'avatar.driver.gallery':
+ /* I couldn't figure out how to set these up so they are probably wrong */
+ $filepath = $this->app->_session['more_info']['convertMembers']['gallery_location'];
+ $filename = $row['user_avatar'];
+ break;
+ }
+ }
+
+ /* Profile Fields */
+ $pfields = array();
+ try
+ {
+ $userfields = $this->db->select( '*', 'profile_fields_data', array( "user_id=?", $row['user_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {}
+
+ foreach( $this->db->select( '*', 'profile_fields' ) AS $field )
+ {
+ /* if this is a select field, we need to pull the value from profile_fields_lang */
+ $field_type = explode( '.', $field['field_type'] );
+ $field_type = array_pop( $field_type );
+
+ if ( isset( $userfields['pf_' . $field['field_name'] ] ) )
+ {
+ if ( $field_type == 'dropdown' )
+ {
+ /* phpBB stores the option incremented from 1 - however the options are stored incremented from 0. So if you select the first option, 1 is stored but profile_fields_lang actually has it as 0. I don't get it either. */
+ $pfields[ $field['field_id'] ] = $this->db->select( 'lang_value', 'profile_fields_lang', array( "field_id=? AND option_id=?", $field['field_id'], $userfields['pf_' . $field['field_name'] ] - 1 ) )->first();
+ }
+ else
+ {
+ $pfields[ $field['field_id'] ] = $userfields['pf_' . $field['field_name'] ];
+ }
+ }
+ }
+
+ /* Finally */
+ $libraryClass->convertMember( $info, $pfields, $filename, $filepath );
+
+ /* Friends */
+ foreach( $this->db->select( '*', 'zebra', array( "user_id=? AND friend=?", $row['user_id'], 1 ) ) AS $follower )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'members',
+ 'follow_rel_id' => $follower['zebra_id'],
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $follower['user_id'],
+ ) );
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'warnings', array( "user_id=? AND post_id<>?", $row['user_id'], 0 ) ) AS $warn )
+ {
+ try
+ {
+ $log = $this->db->select( '*', 'log', array( "log_id=?", $warn['log_id'] ) )->first();
+ $data = @\unserialize( $log['log_data'] );
+ }
+ catch( \UnderflowException $e )
+ {
+ $log = array( 'user_id' => 0 );
+ $data = array( 0 => NULL );
+ }
+
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['warning_id'],
+ 'wl_member' => $warn['user_id'],
+ 'wl_moderator' => $log['user_id'],
+ 'wl_date' => $warn['warning_time'],
+ 'wl_points' => 1,
+ 'wl_note_member' => isset( $data[0] ) ? $data[0] : NULL,
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['user_id'] );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'zebra', 'user_id', array( "foe=?", 1 ) ) AS $row )
+ {
+ $info = array(
+ 'ignore_id' => $row['user_id'] . '-' . $row['zebra_id'],
+ 'ignore_owner_id' => $row['user_id'],
+ 'ignore_ignore_id' => $row['zebra_id'],
+ );
+
+ foreach( \IPS\core\Ignore::types() AS $type )
+ {
+ $info['ignore_' . $type] = 1;
+ }
+
+ $libraryClass->convertIgnoredUser( $info );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'rank_id' );
+
+ foreach( $this->fetch( 'ranks', 'rank_id', array( "rank_special=?", 0 ) ) AS $row )
+ {
+ $filepath = NULL;
+ if ( $row['rank_image'] )
+ {
+ $filepath = rtrim( $this->app->_session['more_info']['convertRanks']['rank_location'], '/' ) . '/' . $row['rank_image'];
+ }
+
+ $libraryClass->convertRank( array(
+ 'id' => $row['rank_id'],
+ 'title' => $row['rank_title'],
+ 'posts' => $row['rank_min'],
+ ), $filepath );
+
+ $libraryClass->setLastKeyValue( $row['rank_id'] );
+ }
+ }
+
+ /**
+ * Convert profannity filters
+ *
+ * @return void
+ */
+ public function convertProfanityFilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'word_id' );
+
+ foreach( $this->fetch( 'words', 'word_id' ) AS $row )
+ {
+ $libraryClass->convertProfanityFilter( array(
+ 'wid' => $row['word_id'],
+ 'type' => $row['word'],
+ 'swop' => $row['replacement']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['word_id'] );
+ }
+ }
+
+ /**
+ * Convert ban filters
+ *
+ * @return void
+ */
+ public function convertBanfilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'ban_id' );
+
+ foreach( $this->fetch( 'banlist', 'ban_id', array( "ban_userid=?", 0 ) ) AS $row )
+ {
+ $type = NULL;
+ if ( $row['ban_ip'] )
+ {
+ $type = 'ip';
+ }
+ else if ( $row['ban_email'] )
+ {
+ $type = 'email';
+ }
+
+ /* If Type is null - skip */
+ if ( is_null( $type ) )
+ {
+ $libraryClass->setLastKeyValue( $row['ban_id'] );
+ continue;
+ }
+
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => 'b' . $row['ban_id'],
+ 'ban_type' => $type,
+ 'ban_content' => ( $type == 'ip' ) ? $row['ban_ip'] : $row['ban_email'],
+ 'ban_date' => $row['ban_start'],
+ 'ban_reason' => $row['ban_give_reason']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['ban_id'] );
+ }
+ }
+
+ /**
+ * Convert ban filters (second time)
+ *
+ * @return void
+ */
+ public function convertBanfilters2()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'disallow_id' );
+
+ foreach( $this->fetch( 'disallow', 'disallow_id' ) AS $row )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => 'd' . $row['disallow_id'],
+ 'ban_content' => $row['disallow_username'],
+ 'ban_type' => 'name',
+ 'ban_date' => time(),
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['disallow_id'] );
+ }
+ }
+
+ /**
+ * Convert PMs
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'msg_id' );
+
+ foreach( $this->fetch( 'privmsgs', 'msg_id' ) AS $row )
+ {
+ /* Set up the topic */
+ $topic = array(
+ 'mt_id' => $row['msg_id'],
+ 'mt_date' => $row['message_time'],
+ 'mt_title' => $row['message_subject'],
+ 'mt_starter_id' => $row['author_id'],
+ 'mt_start_time' => $row['message_time'],
+ 'mt_last_post_time' => $row['message_time'],
+ );
+
+ /* Now the maps */
+ $maps = array();
+
+ /* First one first initial author */
+ $maps[$row['author_id']] = array(
+ 'map_user_id' => $row['author_id'],
+ 'map_is_starter' => 1,
+ 'map_last_topic_reply' => $row['message_time'],
+ );
+
+ /* Fetch recipients for the message if it has been moved to a custom folder */
+ $messageTo = explode( ':', $row['to_address'] );
+
+ foreach( $messageTo as $messageRecipient )
+ {
+ $messageRecipient = (int) mb_substr( $messageRecipient, 2 );
+
+ $maps[ $messageRecipient ] = array(
+ 'map_user_id' => $messageRecipient,
+ 'map_is_starter' => 0,
+ 'map_last_topic_reply' => $row['message_time'],
+ );
+ }
+
+ /* Now everyone else */
+ foreach( $this->db->select( '*', 'privmsgs_to', array( "msg_id=?", $row['msg_id'] ) ) AS $to )
+ {
+ /* If the user already exists just skip. This wouldn't normally matter, however if the sender has moved the message
+ to a custom folder then they will have a row in privmsgs_to and we will end up resetting map_is_starter to 0 otherwise */
+ if( isset( $maps[ $to['user_id'] ] ) )
+ {
+ continue;
+ }
+
+ $maps[ $to['user_id'] ] = array(
+ 'map_user_id' => $to['user_id'],
+ 'map_is_starter' => 0,
+ 'map_last_topic_reply' => $row['message_time']
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['msg_id'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'msg_id' );
+
+ foreach( $this->fetch( 'privmsgs', 'msg_id' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['msg_id'],
+ 'msg_topic_id' => $row['msg_id'],
+ 'msg_post' => static::stripUid( $row['message_text'], $row['bbcode_uid'] ),
+ 'msg_date' => $row['message_time'],
+ 'msg_author_id' => $row['author_id'],
+ 'msg_ip_address' => $row['author_ip'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['msg_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'memberlist.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->u, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Phpmyforum.php b/applications/convert/sources/Software/Core/Phpmyforum.php
new file mode 100644
index 00000000..e73813be
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Phpmyforum.php
@@ -0,0 +1,86 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 6 December 2016
+ * @note Only redirect scripts are supported right now
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Phpmyforum extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "PhpMyForum";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "phpmyforum";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return NULL
+ */
+ public static function canConvert()
+ {
+ return NULL;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'profile.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->id, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Punbb.php b/applications/convert/sources/Software/Core/Punbb.php
new file mode 100644
index 00000000..54da01b2
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Punbb.php
@@ -0,0 +1,448 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Punbb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "PunBB 1.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "punbb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGroups' => array(
+ 'table' => 'groups',
+ 'where' => NULL,
+ ),
+ 'convertMembers' => array(
+ 'table' => 'users',
+ 'where' => NULL,
+ ),
+ 'convertRanks' => array(
+ 'table' => 'ranks',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'messages',
+ 'where' => NULL,
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'messages',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGroups',
+ 'convertMembers'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'groups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['g_id']}"] = $group['g_title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['g_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['g_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ /* Pseudo Profile Fields */
+ foreach( [ 'url', 'jabber', 'icq', 'msn', 'aim', 'yahoo', 'location' ] AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => ucwords( $field ) ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ $post = nl2br( $post );
+
+ $post = preg_replace( "#\[quote=(.+)\]#", "[quote name=$1]", $post );
+ return $post;
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'g_id' );
+
+ foreach( $this->fetch( 'groups', 'g_id' ) AS $row )
+ {
+ $info = array(
+ 'g_id' => $row['g_id'],
+ 'g_name' => $row['g_title'],
+ 'g_view_board' => $row['g_read_board'],
+ 'g_mem_info' => $row['g_view_users'],
+ 'g_is_supmod' => $row['g_moderator'],
+ 'g_use_search' => $row['g_search'],
+ 'g_edit_posts' => $row['g_edit_posts'],
+ 'g_use_pm' => $row['g_pm'],
+ 'g_pm_flood_mins' => ( $row['g_email_flood'] ) ? ceil( $row['g_email_flood'] / 60 ) : 0,
+ 'g_delete_own_posts' => $row['g_delete_posts'],
+ 'g_avoid_flood' => ( $row['g_post_flood'] == 0 ) ? 1 : 0,
+ 'g_max_messages' => $row['g_pm_limit'],
+ 'prefix' => ( $row['g_color'] ) ? "" : NULL,
+ 'suffix' => ( $row['g_color'] ) ? " " : NULL,
+ 'g_search_flood' => $row['g_search_flood'],
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['g_id']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['g_id']}"] : NULL;
+
+ $libraryClass->convertGroup( $info );
+
+ $libraryClass->setLastKeyValue( $row['g_id'], $merge );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'users', 'id' ) AS $row )
+ {
+ $info = array(
+ 'member_id' => $row['id'],
+ 'email' => $row['email'],
+ 'name' => $row['username'],
+ 'password' => $row['password'],
+ 'conv_password_extra' => $row['salt'],
+ 'member_group_id' => $row['group_id'],
+ 'joined' => $row['registered'],
+ 'ip_address' => $row['registration_ip'],
+ 'last_visit' => $row['last_visit'],
+ 'last_activity' => $row['last_visit'],
+ 'auto_track' => ( $row['auto_notify'] ) ? [ 'content' => 1, 'comments' => 1, 'method' => 'immediate' ] : 0,
+ 'members_bitoptions' => [ 'view_sigs' => $row['show_sig'] ],
+ 'signature' => $row['signature'],
+ 'timezone' => $row['timezone'],
+ 'member_title' => $row['title'],
+ 'member_posts' => $row['num_posts'],
+ );
+
+ /* Pseudo Fields */
+ $profileFields = array();
+ foreach( [ 'url', 'jabber', 'icq', 'msn', 'aim', 'yahoo', 'location' ] AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $profileFields[$pseudo] = $row[$pseudo];
+ }
+
+ $libraryClass->convertMember( $info, $profileFields );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'ranks', 'id' ) AS $row )
+ {
+ $info = array(
+ 'id' => $row['id'],
+ 'title' => $row['rank'],
+ 'posts' => $row['min_posts']
+ );
+
+ $libraryClass->convertRank( $info );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert private messages
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'messages', 'id' ) AS $row )
+ {
+ /* PunBB does not have conversations, so convert each one into and individual PM topic */
+ $topic = array(
+ 'mt_id' => $row['id'],
+ 'mt_date' => $row['posted'],
+ 'mt_title' => $row['subject'],
+ 'mt_starter_id' => $row['sender_id'],
+ 'mt_start_time' => $row['posted'],
+ 'mt_last_post_time' => $row['posted'],
+ 'mt_to_count' => ( $row['sender_id'] == $row['owner'] ) ? 1 : 2,
+ 'mt_replies' => 1,
+ );
+
+ $maps = array();
+
+ /* Sender */
+ $maps[$row['sender_id']] = array(
+ 'map_user_id' => $row['sender_id'],
+ 'map_is_starter' => 1,
+ 'map_last_topic_reply' => $row['posted'],
+ );
+
+ /* Recipient... if it isn't the sender */
+ if ( $row['sender_id'] != $row['owner'] )
+ {
+ $maps[$row['owner']] = array(
+ 'map_user_id' => $row['owner'],
+ 'map_is_starter' => 0,
+ 'map_last_topic_reply' => $row['posted']
+ );
+ }
+
+ /* Simples! */
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'messages', 'id' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['id'],
+ 'msg_topic_id' => $row['id'],
+ 'msg_date' => $row['posted'],
+ 'msg_post' => $row['message'],
+ 'msg_author_id' => $row['sender_id'],
+ 'msg_ip_address' => $row['sender_ip'],
+ 'msg_is_first_post' => 1,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'profile.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->u, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Smf.php b/applications/convert/sources/Software/Core/Smf.php
new file mode 100644
index 00000000..2c048d2a
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Smf.php
@@ -0,0 +1,894 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2015 Invision Power Services, Inc.
+ * @package IPS Social Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Smf extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Simple Machines Forum";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "smf";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertBanfilters' => array(
+ 'table' => 'ban_items',
+ 'where' => NULL,
+ ),
+ 'convertEmoticons' => array(
+ 'table' => 'smileys',
+ 'where' => NULL,
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'custom_fields',
+ 'where' => NULL,
+ ),
+ 'convertGroups' => array(
+ 'table' => 'membergroups',
+ 'where' => NULL,
+ ),
+ 'convertMembers' => array(
+ 'table' => 'members',
+ 'where' => NULL,
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'personal_messages',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'personal_messages',
+ 'where' => NULL
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'members',
+ 'where' => array( 'LENGTH( pm_ignore_list ) > 0' ),
+ ),
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers'
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'ignored_users':
+ try
+ {
+ return $this->db->select( 'SUM(CHAR_LENGTH(buddy_list) - CHAR_LENGTH(REPLACE(buddy_list, ",", "") + 1))', 'members', array( "pm_ignore_list!=?", '' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return 0;
+ }
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ $return['convertEmoticons']['emoticon_path'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ $return['convertEmoticons']['keep_existing_emoticons'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ );
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'custom_fields' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['id_field']}"] = $field['field_name'];
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['id_field']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['id_field']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'membergroups' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['id_group']}"] = $group['group_name'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['id_group']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['id_group']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ $return['convertMembers']['username_or_display_name'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'user_name',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'user_name', 'display_name' ) ),
+ 'field_hint' => NULL,
+ );
+
+ \IPS\Member::loggedIn()->language()->words['photo_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'photo_location_nodb_desc' );
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => "This is typically: /path/to/smf/attachments/",
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ $return['convertMembers']['gallery_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => "This is typically: /path/to/smf/uploads/avatars/",
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ foreach( array( 'gender', 'website_url', 'location', 'icq', 'aim', 'yim', 'msn', 'personal_text' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return $return[ $method ];
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Message to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ /* Attachments */
+ \IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ // Replace BBCode - The table HTML is removed by our parser, but it leaves the posts cleaner than having unparsed bbcode
+ $search = array( '[li]', '[/li]', '[table]', '[/table]', '[tr]', '[/tr]', '[td]', '[/td]', '[tt]', '[/tt]' );
+ $replace = array( '[*]', '', '', '', ' ', '', ' ', '[code]', '[/code]' );
+
+ $post = str_replace( $search, $replace, $post );
+
+ // And img tags
+ $post = preg_replace("#\[img width=(\d+) height=(\d+)\]([a-zA-Z0-9.:\-/]+)\[\/img\]#i", "[img]$3[/img]", $post);
+
+ //Quotes
+ $post = preg_replace("#\[quote author=(.+?) link=(.+?)=(\d+)\.msg(\d+)(.+?)date=(\d+)\](.+)\[\/quote\]#i", "[quote name=\"$1\" post=\"$4\" timestamp=\"$6\"]$7[/quote]", $post);
+
+ return $post;
+ }
+
+ /**
+ * Convert ban filters
+ *
+ * @return void
+ */
+ public function convertBanfilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_ban' );
+
+ foreach( $this->fetch( 'ban_items', 'id_ban' ) AS $row )
+ {
+ /* Hostnames aren't supported, and Members are done later */
+ if ( $row['hostname'] OR $row['id_member'] )
+ {
+ $libraryClass->setLastKeyValue( $row['id_ban'] );
+ continue;
+ }
+
+ if ( $row['ip_low1'] AND $row['ip_low2'] AND $row['ip_low3'] AND $row['ip_low4'] )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row['id_ban'],
+ 'ban_type' => 'ip',
+ 'ban_content' => $row['ip_low1'] . '.' . $row['ip_low2'] . '.' . $row['ip_low3'] . '.' . $row['ip_low4']
+ ) );
+ }
+
+ if ( $row['email_address'] )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row['id_ban'],
+ 'ban_type' => 'email',
+ 'ban_content' => $row['email_address'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['id_ban'] );
+ }
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_smiley' );
+
+ foreach( $this->fetch( 'smileys', 'id_smiley' ) AS $row )
+ {
+ $libraryClass->convertEmoticon( array(
+ 'id' => $row['id_smiley'],
+ 'typed' => $row['code'],
+ 'filename' => $row['filename'],
+ 'clickable' => !$row['hidden'],
+ 'emo_position' => $row['smiley_order']
+ ), array(
+ 'set' => md5( 'Converted' ),
+ 'title' => 'Converted',
+ 'position' => 1
+ ), $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) );
+
+ $libraryClass->setLastKeyValue( $row['id_smiley'] );
+ }
+ }
+
+ /**
+ * Convert private messages
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_pm' );
+
+ foreach( $this->fetch( 'personal_messages', 'id_pm' ) AS $row )
+ {
+ $topic = array(
+ 'mt_id' => $row['id_pm'],
+ 'mt_date' => $row['msgtime'],
+ 'mt_title' => $row['subject'],
+ 'mt_starter_id' => $row['id_member_from'],
+ 'mt_start_time' => $row['msgtime'],
+ 'mt_last_post_time' => $row['msgtime'],
+ 'mt_to_count' => 0,
+ 'mt_replies' => 0,
+ );
+
+ $maps = array(
+ $row['id_member_from'] = array(
+ 'map_user_id' => $row['id_member_from'],
+ 'map_read_time' => time(),
+ 'map_user_active' => ( $row['deleted_by_sender'] ) ? 0 : 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => 0,
+ 'map_is_starter' => 1,
+ 'map_last_topic_reply' => $row['msgtime']
+ )
+ );
+
+ foreach( $this->db->select( '*', 'pm_recipients', array( "id_pm=?", $row['id_pm'] ) ) AS $map )
+ {
+ if( !isset( $maps[ $map['id_member'] ] ) )
+ {
+ $maps[ $map['id_member'] ] = array(
+ 'map_user_id' => $map['id_member'],
+ 'map_read_time' => $map['is_new'] ? time() : 0,
+ 'map_user_active' => $map['deleted'] ? 0 : 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => $map['is_new'],
+ 'map_is_starter' => 0,
+ 'map_last_topic_reply' => $row['msgtime']
+ );
+ }
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['id_pm'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_pm' );
+
+ foreach( $this->fetch( 'personal_messages', 'id_pm' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['id_pm'],
+ 'msg_topic_id' => $row['id_pm'],
+ 'msg_date' => $row['msgtime'],
+ 'msg_post' => $row['body'],
+ 'msg_author_id' => $row['id_member_from'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['id_pm'] );
+ }
+ }
+
+ /**
+ * Convert custom fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_field' );
+
+ foreach( $this->fetch( 'custom_fields', 'id_field' ) AS $row )
+ {
+ $type = NULL;
+ switch( $row['field_type'] )
+ {
+ case 'textarea':
+ $type = 'TextArea';
+ break;
+
+ case 'check':
+ $type = 'Checkbox';
+ break;
+
+ default:
+ $type = ucwords( $row['field_type'] );
+ break;
+ }
+
+ if ( $row['mask'] == 'email' )
+ {
+ $type = 'Email';
+ }
+
+ if ( $row['mask'] == 'number' )
+ {
+ $type = 'Number';
+ }
+
+ $info = array(
+ 'pf_id' => $row['col_name'],
+ 'pf_type' => $type,
+ 'pf_name' => $row['field_name'],
+ 'pf_desc' => $row['field_desc'],
+ 'pf_content' => ( in_array( $row['field_type'], array( 'select', 'radio' ) ) ) ? json_encode( explode( ',', $row['field_options'] ) ) : NULL,
+ 'pf_not_null' => ( $row['show_reg'] == 2 ) ? 1 : 0,
+ 'pf_member_hide' => ( $row['private'] >= 3 ) ? 1 : 0,
+ 'pf_max_input' => ( !in_array( $row['field_type'], array( 'select', 'radio', 'check' ) ) ) ? $row['field_length'] : NULL,
+ 'pf_member_edit' => ( $row['private'] < 4 ) ? 1 : 0,
+ 'pf_position' => $row['placement'],
+ 'pf_show_on_reg' => ( $row['show_reg'] >= 1 ) ? 1 : 0,
+ 'pf_input_format' => ( mb_substr( $row['mask'], 0, 5 ) == 'regex' ) ? str_replace( 'regex', '', $row['mask'] ) : NULL,
+ 'pf_admin_only' => ( $row['private'] == 4 ) ? 1 : 0,
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['id_field']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['id_field']}"] : NULL;
+
+ $libraryClass->convertProfileField( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['id_field'] );
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_group' );
+
+ foreach( $this->fetch( 'membergroups', 'id_group' ) AS $row )
+ {
+ $prefix = NULL;
+ $suffix = NULL;
+
+ if ( $row['online_color'] )
+ {
+ $prefix = "";
+ $suffix = " ";
+ }
+
+ $info = array(
+ 'g_id' => $row['id_group'],
+ 'g_name' => $row['group_name'],
+ 'prefix' => $prefix,
+ 'suffix' => $suffix,
+ 'g_max_messages' => $row['max_messages'],
+ );
+
+ $merge = $this->app->_session['more_info']['convertGroups']["map_group_{$row['id_group']}"] != 'none' ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['id_group']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['id_group'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_member' );
+
+ foreach( $this->fetch( 'members', 'id_member' ) AS $row )
+ {
+ $name = $row['real_name'];
+
+ if ( $this->app->_session['more_info']['convertMembers']['username_or_display_name'] == 'username' )
+ {
+ $name = $row['member_name'];
+ }
+
+ /* Restrictions */
+ $ban = 0;
+ $rp = 0;
+ try
+ {
+ $ban_data = $this->db->select( '*', 'ban_groups', array( "id_ban_group=?", $this->db->select( 'id_ban_group', 'ban_items', array( "id_member=?", $row['id_member'] ) )->first() ) )->first();
+
+ $time = -1;
+ if ( $ban_data['expire_time'] )
+ {
+ $time = $ban_data['expire_time'];
+ }
+
+ if ( $ban_data['cannot_access'] )
+ {
+ $ban = $time;
+ }
+
+ if ( $ban_data['cannot_post'] )
+ {
+ $rp = $time;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ $bday_year = $bday_month = $bday_day = NULL;
+
+ /* Make sure it isn't the SMF placeholder */
+ if( $row['birthdate'] != '0001-01-01' )
+ {
+ list( $bday_year, $bday_month, $bday_day ) = explode( '-', $row['birthdate'] );
+ }
+
+ $info = array(
+ 'member_id' => $row['id_member'],
+ 'email' => $row['email_address'],
+ 'name' => $name,
+ 'password' => $row['passwd'],
+ 'password_extra' => $row['password_salt'],
+ 'member_group_id' => $row['id_group'],
+ 'joined' => $row['date_registered'],
+ 'ip_address' => $row['member_ip'],
+ 'msg_count_new' => $row['unread_messages'],
+ 'msg_count_total' => $row['instant_messages'],
+ 'msg_show_notification' => $row['new_pm'],
+ 'last_visit' => $row['last_login'],
+ 'last_activity' => $row['last_login'],
+ 'restrict_post' => $rp,
+ 'temp_ban' => $ban,
+ 'bday_day' => $bday_day,
+ 'bday_month' => $bday_month,
+ 'bday_year' => $bday_year,
+ 'mgroup_others' => $row['additional_groups'],
+ 'signature' => $row['signature'],
+ 'pp_reputation_points' => $row['karma_good'] - $row['karma_bad'],
+ 'timezone' => $row['time_offset'],
+ 'allow_admin_mails' => $row['notify_announcements'],
+ 'member_title' => $row['usertitle'],
+ 'member_posts' => $row['posts'],
+ );
+
+ $pfields = array();
+
+ /* Pseudo Profile Fields */
+ foreach( array( 'gender', 'website_url', 'location', 'icq', 'aim', 'yim', 'msn', 'personal_text' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $type = 'Text';
+ $content = '[]';
+ switch( $pseudo )
+ {
+ case 'gender':
+ $type = 'Select';
+ $content = json_encode( [ 'Male', 'Female', 'Undisclosed' ] );
+ break;
+
+ case 'personal_text':
+ $type = 'TextArea';
+ break;
+ }
+
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => $type,
+ 'pf_content' => $content,
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ switch( $pseudo )
+ {
+ case 'gender':
+ switch( $row[$pseudo] )
+ {
+ case 1:
+ $pfields[$pseudo] = 'Male';
+ break;
+
+ case 2:
+ $pfields[$pseudo] = 'Female';
+ break;
+
+ default:
+ $pfields[$pseudo] = 'Undisclosed';
+ break;
+ }
+ break;
+ default:
+ $pfields[$pseudo] = $row[$pseudo];
+ break;
+ }
+ }
+
+ /* Real Profile Fields */
+ foreach( $this->db->select( 'variable, value', 'themes', array( "id_member=?", $row['id_member'] ) )->setKeyField( 'variable' )->setValueField( 'value' ) AS $key => $value )
+ {
+ $pfields[$key] = $value;
+ }
+
+ /* Photos */
+ $filedata = NULL;
+ $filename = NULL;
+ $filepath = NULL;
+ if ( $row['avatar'] )
+ {
+ if ( mb_substr( $row['avatar'], 0, 4 ) == 'http' )
+ {
+ /* Remote */
+ try
+ {
+ $file = \IPS\Http\Url::external( $row['avatar'] )->request()->get();
+
+ $filename = explode( '/', $row['avatar'] );
+ $filename = array_pop( $filename );
+ $filedata = (string) $file;
+ }
+ catch( \Exception $e ) {}
+ }
+ else
+ {
+ /* Gallery */
+ $file = rtrim( $this->app->_session['more_info']['convertMembers']['gallery_location'], '/' );
+ if ( @file_exists( $file . '/' . $row['avatar'] ) )
+ {
+ $filename = $row['avatar'];
+ $filepath = $file;
+ }
+ }
+ }
+ else
+ {
+ /* Maybe Uploaded */
+ try
+ {
+ $attach = $this->db->select( '*', 'attachments', array( "id_member=? AND filename LIKE CONCAT( ?, '%' )", $row['id_member'], 'avatar_' ) )->first();
+
+ $file = rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' );
+
+ /* We need to figure out where it is */
+ if ( $attach['file_hash'] )
+ {
+ $location = $attach['id_attach'] . '_' . $attach['file_hash'];
+ }
+ else
+ {
+ /* Clean filename per legacy SMF requirements */
+ $cleanName = strtr( $attach['filename'], 'ŠŽšžŸÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ', 'SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy');
+ $cleanName = strtr( $cleanName, array( 'Þ' => 'TH', 'þ' => 'th', 'Ð' => 'DH', 'ð' => 'dh', 'ß' => 'ss', 'Œ' => 'OE', 'œ' => 'oe', 'Æ' => 'AE', 'æ' => 'ae', 'µ' => 'u' ) );
+ $cleanName = preg_replace( array( '/s/', '/[^w_.-]/' ), array( '_', '' ), $cleanName );
+ $location = $attach['id_attach'] . '_' . str_replace( '.', '_', $cleanName ) . md5( $cleanName );
+ }
+
+ if ( @file_exists( $file . '/' . $location ) )
+ {
+ $filename = $attach['filename'];
+ $filedata = file_get_contents( $file . '/' . $location );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $libraryClass->convertMember( $info, $pfields, $filename, $filepath, $filedata );
+
+ $libraryClass->setLastKeyValue( $row['id_member'] );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_member' );
+
+ foreach( $this->fetch( 'members', 'id_member', array( 'LENGTH( pm_ignore_list ) > 0' ) ) AS $row )
+ {
+ if ( !$row['pm_ignore_list'] )
+ {
+ continue;
+ }
+
+ foreach( explode( ',', $row['pm_ignore_list'] ) AS $id )
+ {
+ $info = array();
+ $info['ignore_id'] = $row['id_member'] . '_' . $id;
+ $info['ignore_owner_id'] = $row['id_member'];
+ $info['ignore_ignore_id'] = $id;
+
+ foreach( \IPS\core\Ignore::types() AS $type )
+ {
+ $info['ignore_' . $type] = 1;
+ }
+ $libraryClass->convertIgnoredUser( $info );
+ }
+
+ $libraryClass->setLastKeyValue( $row['id_member'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ * @note Current profile URL format: /index.php?action=profile;u=123
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ if( isset( \IPS\Request::i()->action ) AND mb_strpos( \IPS\Request::i()->action, 'profile' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->u ) )
+ {
+ $oldId = \IPS\Request::i()->u;
+ }
+ else
+ {
+ $pieces = explode( ';', \IPS\Request::i()->action );
+
+ foreach( $pieces as $piece )
+ {
+ $_pieces = explode( '=', $piece );
+
+ if( $_pieces[0] === 'u' )
+ {
+ $oldId = (int) $_pieces[1];
+ break;
+ }
+ }
+ }
+
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/UBBthreads.php b/applications/convert/sources/Software/Core/UBBthreads.php
new file mode 100644
index 00000000..9317bbc3
--- /dev/null
+++ b/applications/convert/sources/Software/Core/UBBthreads.php
@@ -0,0 +1,776 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2015 Invision Power Services, Inc.
+ * @package IPS Social Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _UBBthreads extends \IPS\convert\Software
+{
+ /**
+ * @brief Emoticons WHERE statement
+ * @see convertEmoticons()
+ */
+ protected static $emoticonsWhere = 'GRAEMLIN_IS_ACTIVE=1';
+
+ /**
+ * @brief Groups WHERE statement
+ * @see convertGroups()
+ */
+ protected static $groupsWhere = 'GROUP_IS_DISABLED=0';
+
+ /**
+ * @brief Ignored users WHERE statement
+ * @see convertIgnoredUsers()
+ */
+ protected static $ignoredUsersWhere = "USER_IGNORE_LIST IS NOT NULL AND USER_IGNORE_LIST NOT IN ( '', '-' )";
+
+ /**
+ * @brief Members WHERE statement
+ * @see convertMembers()
+ */
+ protected static $membersWhere = array( 'u.USER_LOGIN_NAME<>?', '**DONOTDELETE**' );
+
+ /**
+ * This is.. kind of hacky, but it's used so we can try and support non-exact profanity matches without converting
+ * other regexes we don't support
+ *
+ * @brief Profanity filters WHERE statement
+ * @see convertProfanityFilters()
+ */
+ protected static $profanityFiltersWhere = '(
+ CENSOR_WORD NOT LIKE "%(.*)%" AND (
+ CENSOR_WORD NOT LIKE "%(.*?)%" OR (
+ CENSOR_WORD LIKE "%(.*?)" AND CENSOR_WORD NOT LIKE "%(.*?)%(.*?)"
+ )
+ )
+ )';
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "UBBthreads";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "UBBthreads";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertEmoticons' => array(
+ 'table' => 'GRAEMLINS',
+ 'where' => static::$emoticonsWhere,
+ ),
+ 'convertBanfilters' => array(
+ 'table' => 'pseudo_banfilters', /** @see countRows() */
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'GROUPS',
+ 'where' => static::$groupsWhere
+ ),
+ 'convertMembers' => array(
+ 'table' => array( 'USERS', 'u' ),
+ 'where' => static::$membersWhere,
+ 'extra_steps' => array( 'convertMembersFollowers' ),
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'USER_PROFILE',
+ 'where' => static::$ignoredUsersWhere
+ ),
+ 'convertMembersFollowers' => array(
+ 'table' => 'WATCH_LISTS',
+ 'where' => array( 'WATCH_TYPE=?', 'u' )
+ ),
+ 'convertRanks' => array(
+ 'table' => 'USER_TITLES',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'PRIVATE_MESSAGE_TOPICS',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'PRIVATE_MESSAGE_POSTS',
+ 'where' => NULL
+ ),
+ 'convertProfanityFilters' => array(
+ 'table' => 'CENSOR_LIST',
+ 'where' => static::$profanityFiltersWhere
+ ),
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch ( $table )
+ {
+ case 'pseudo_banfilters':
+ return parent::countRows( 'BANNED_EMAILS' )
+ + parent::countRows( 'BANNED_HOSTS' )
+ + parent::countRows( 'RESERVED_NAMES' );
+ }
+
+ return parent::countRows( $table, $where, $recache );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertGroups',
+ 'convertMembers'
+ );
+ }
+
+ /**
+ * Attempt to convert a textual date(time) representation to a DateTime instance
+ *
+ * @param string $date
+ * @return \IPS\DateTime|null
+ */
+ protected function stringToDateTime( $date )
+ {
+ try
+ {
+ return new \IPS\DateTime( $date );
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ $return['convertEmoticons'] = array();
+
+ \IPS\Member::loggedIn()->language()->words['emoticon_path'] = \IPS\Member::loggedIn()->language()->addToStack( 'source_path', FALSE, array( 'sprintf' => array( 'UBBthreads' ) ) );
+ $return['convertEmoticons']['emoticon_path'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL
+ );
+ $return['convertEmoticons']['keep_existing_emoticons'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ );
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[ $group->g_id ] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'GROUPS' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['GROUP_ID']}"] = $group['GROUP_NAME'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['GROUP_ID']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['GROUP_ID']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* Should we use the username or display name property when converting? */
+ $return['convertMembers']['username'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'display_name',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'username' => \IPS\Member::loggedIn()->language()->addToStack( 'user_name' ), 'display_name' => \IPS\Member::loggedIn()->language()->addToStack( 'display_name' ) ) ),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'username_hint' ),
+ );
+
+ foreach( array( 'homepage', 'occupation', 'hobbies', 'location', 'icq', 'yahoo', 'aim', 'msn' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\core\Messenger\Conversation' ), 3, array( 'class' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ /* Attachments */
+ \IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ $post = preg_replace( "#\[quote=(.+?)\]#i", "[quote name=\"$1\"]", $post );
+ $post = str_ireplace( [ "[image]", '[/image]', '[size:', '[color:' ], [ '[img]', '[/img]', '[size=', '[color=' ], $post );
+
+ return $post;
+ }
+
+ /**
+ * Convert ban filters
+ *
+ * @return void
+ */
+ public function convertBanfilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ /* Banned e-mails */
+ foreach ( $this->db->select( 'BANNED_EMAIL', 'BANNED_EMAILS' ) as $row )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row, // We don't actually have an ID column
+ 'ban_type' => 'email',
+ 'ban_content' => str_replace( '%', '*', $row ) // Replace UBB's wildcard character
+ ) );
+ }
+
+ /* Banned IP's */
+ foreach( $this->db->select( 'BANNED_HOST', 'BANNED_HOSTS' ) as $row )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row, // We don't actually have an ID column
+ 'ban_type' => 'ip',
+ 'ban_content' => str_replace( '%', '*', $row ) // Replace UBB's wildcard character
+ ) );
+ }
+
+ /* Banned / "reserved" names */
+ foreach ( $this->db->select( 'RESERVED_USERNAME', 'RESERVED_NAMES' ) as $row )
+ {
+ $libraryClass->convertBanfilter( array(
+ 'ban_id' => $row, // We don't actually have an ID column
+ 'ban_type' => 'name',
+ 'ban_content' => str_replace( '%', '*', $row ) // Replace UBB's wildcard character; TODO: Verify UBB actually supports wildcards here
+ ) );
+ }
+
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'GRAEMLINS', 'GRAEMLIN_ID', static::$emoticonsWhere ) as $row )
+ {
+ $info = array(
+ 'id' => $row['GRAEMLIN_ID'],
+ 'typed' => $row['GRAEMLIN_SMILEY_CODE'] ?: ':'.$row['GRAEMLIN_MARKUP_CODE'].':',
+ 'width' => $row['GRAEMLIN_WIDTH'],
+ 'height' => $row['GRAEMLIN_HEIGHT'],
+ 'filename' => $row['GRAEMLIN_IMAGE'],
+ 'emo_position' => $row['GRAEMLIN_ID'],
+ );
+
+ $set = array(
+ 'set' => md5( 'Converted' ),
+ 'title' => 'Converted',
+ 'position' => 1,
+ );
+
+ $libraryClass->convertEmoticon(
+ $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'],
+ rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . "/images/graemlins/default"
+ );
+ $libraryClass->setLastKeyValue( $row['GRAEMLIN_ID'] );
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'GROUP_ID' );
+
+ foreach ( $this->fetch( 'GROUPS', 'GROUP_ID', static::$groupsWhere ) as $row )
+ {
+ /* @TODO: Can we enable custom titles per group? */
+ $info = array(
+ 'g_id' => $row['GROUP_ID'],
+ 'g_name' => $row['GROUP_NAME'],
+ );
+
+ $merge = $this->app->_session['more_info']['convertGroups']["map_group_{$row['GROUP_ID']}"] != 'none' ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['GROUP_ID']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+ $libraryClass->setLastKeyValue( $row['GROUP_ID'] );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'USER_ID' );
+
+ foreach ( $this->fetch( 'USER_PROFILE', 'USER_ID', static::$ignoredUsersWhere ) as $row )
+ {
+ /* Proper database modeling? CSV's? JSON? What are those things? */
+ foreach ( explode( '-', $row['USER_IGNORE_LIST'] ) as $ignoredMemberMaybe )
+ {
+ if ( ! $ignoredMemberMaybe )
+ {
+ /* We split an empty string. Fabulous. */
+ continue;
+ }
+
+ $info = array(
+ 'ignore_id' => $row['USER_ID'] . '-' . $ignoredMemberMaybe,
+ 'ignore_owner_id' => $row['USER_ID'],
+ 'ignore_ignore_id' => $ignoredMemberMaybe
+ );
+
+ /* Assume we want to ignore everything by this member */
+ foreach ( \IPS\core\Ignore::types() as $type )
+ {
+ $info[ 'ignore_' . $type ] = 1;
+ }
+
+ $libraryClass->convertIgnoredUser( $info );
+ $libraryClass->setLastKeyValue( $row['USER_ID'] );
+ }
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'u.USER_ID' );
+
+ $select = $this->fetch( array( 'USERS', 'u' ), 'u.USER_ID', static::$membersWhere )
+ ->join( array( 'BANNED_USERS', 'b' ), 'u.USER_ID=b.USER_ID', 'LEFT' )
+ ->join( array( 'USER_PROFILE', 'p' ), 'u.USER_ID=p.USER_ID' )
+ ->join( array( 'USER_DATA', 'd' ), 'u.USER_ID=d.USER_ID' );
+
+ foreach ( $select as $row )
+ {
+ /* Birthday */
+ $birthday = array( 'day' => NULL, 'month' => NULL, 'year' => NULL );
+
+ if ( $birthdayDt = $this->stringToDateTime( $row['USER_BIRTHDAY'] ) )
+ {
+ $birthday['day'] = $birthdayDt->format( 'j' );
+ $birthday['month'] = $birthdayDt->format( 'n' );
+ $birthday['year'] = $birthdayDt->format( 'Y' );
+ }
+
+ /* Member groups */
+ $secondaryGroups = iterator_to_array( $this->db->select( 'GROUP_ID', 'USER_GROUPS', array( 'USER_ID=?', $row['USER_ID'] ) ) );
+ $primaryGroup = array_shift( $secondaryGroups );
+
+ $info = array(
+ 'member_id' => $row['USER_ID'],
+ 'name' => ( $this->app->_session['more_info']['convertMembers']['username'] == 'user_name' )
+ ? $row['USER_LOGIN_NAME']
+ : $row['USER_DISPLAY_NAME'],
+ 'email' => $row['USER_REAL_EMAIL'],
+ 'md5_password' => $row['USER_PASSWORD'],
+ 'member_group_id' => $primaryGroup,
+ 'mgroup_others' => $secondaryGroups,
+ 'joined' => \IPS\DateTime::create()->setTimestamp( $row['USER_REGISTERED_ON'] ),
+ 'ip_address' => $row['USER_REGISTRATION_IP'],
+ 'bday_day' => $birthday['day'],
+ 'bday_month' => $birthday['month'],
+ 'bday_year' => $birthday['year'],
+ 'msg_count_total' => $row['USER_TOTAL_PM'],
+ 'last_visit' => \IPS\DateTime::create()->setTimestamp( $row['USER_LAST_VISIT_TIME'] ),
+ 'last_activity' => \IPS\DateTime::create()->setTimestamp(
+ max( (int) $row['USER_LAST_POST_TIME'], (int) $row['USER_LAST_SEARCH_TIME'] )
+ ),
+ 'allow_admin_mails' => ( $row['USER_ACCEPT_ADMIN_EMAILS'] != 'Off' ),
+ 'member_title' => $row['USER_CUSTOM_TITLE'],
+ 'member_posts' => $row['USER_TOTAL_POSTS'],
+ 'signature' => $row['USER_DEFAULT_SIGNATURE'],
+ 'member_last_post' => \IPS\DateTime::create()->setTimestamp( $row['USER_LAST_POST_TIME'] ),
+ 'temp_ban' => isset( $row['BAN_EXPIRATION'] )
+ ? ( ( (string) $row['BAN_EXPIRATION'] === '0' )
+ ? -1
+ : \IPS\DateTime::create()->setTimestamp( $row['BAN_EXPIRATION'] ) )
+ : NULL,
+ );
+
+ /* Profile fields */
+ $pfields = array();
+ foreach( array( 'homepage', 'occupation', 'hobbies', 'location', 'icq', 'yahoo', 'aim', 'msn' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $fieldColumn = 'USER_' . \strtoupper( $pseudo );
+ $pfields[ $pseudo ] = isset( $row[ $fieldColumn ] ) ? $row[ $fieldColumn ] : NULL;
+ }
+
+ /* Profile photo */
+ $profilePhotoName = NULL;
+ $profilePhotoData = NULL;
+
+ if ( !empty( $row['USER_AVATAR'] ) AND ( $row['USER_AVATAR'] != 'http://' ) )
+ {
+ try
+ {
+ $profilePhotoName = pathinfo( parse_url( $row['USER_AVATAR'], PHP_URL_PATH ), PATHINFO_BASENAME );
+ $profilePhotoData = \IPS\Http\Url::external( $row['USER_AVATAR'] )->request()->get();
+ }
+ catch( \IPS\Http\Request\Exception $e ) { }
+ }
+
+ $libraryClass->convertMember( $info, $pfields, $profilePhotoName, NULL, $profilePhotoData );
+ $libraryClass->setLastKeyValue( $row['USER_ID'] );
+ }
+ }
+
+ /**
+ * Convert member followers
+ *
+ * @return void
+ */
+ public function convertMembersFollowers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach ( $this->fetch( 'WATCH_LISTS', 'WATCH_ID', array( 'WATCH_TYPE=?', 'u' ), 'USER_ID ASC, WATCH_ID ASC' ) as $row )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'member',
+ 'follow_rel_id' => $row['WATCH_ID'],
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $row['USER_ID'],
+ 'follow_notify_freq' => $row['WATCH_NOTIFY_IMMEDIATE'] ? 'immediate' : 'none',
+ ) );
+ }
+ }
+
+ /**
+ * Convert member ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'USER_TITLE_ID' );
+
+ foreach ( $this->fetch( 'USER_TITLES', 'USER_TITLE_ID' ) as $row )
+ {
+ /**
+ * @TODO: We should probably prompt to keep or overwrite existing titles, since this can currently result...
+ * ...in duplicate entries for the same post counts if the local rows are not dropped
+ */
+ $libraryClass->convertRank( array(
+ 'id' => $row['USER_TITLE_ID'],
+ 'title' => $row['USER_TITLE_NAME'],
+ 'posts' => $row['USER_TITLE_POST_COUNT']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['USER_TITLE_ID'] );
+ }
+ }
+
+ /**
+ * Convert private messages
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'TOPIC_ID' );
+
+ foreach ( $this->fetch( 'PRIVATE_MESSAGE_TOPICS', 'TOPIC_ID' ) as $topicRow )
+ {
+ $topic = array(
+ 'mt_id' => $topicRow['TOPIC_ID'],
+ 'mt_date' => \IPS\DateTime::create()->setTimestamp( $topicRow['TOPIC_TIME'] ),
+ 'mt_title' => $topicRow['TOPIC_SUBJECT'],
+ 'mt_starter_id' => $topicRow['USER_ID'],
+ 'mt_start_time' => \IPS\DateTime::create()->setTimestamp( $topicRow['TOPIC_TIME'] ),
+ 'mt_last_post_time' => \IPS\DateTime::create()->setTimestamp( $topicRow['TOPIC_LAST_REPLY_TIME'] ),
+ 'mt_replies' => $topicRow['TOPIC_REPLIES'],
+ );
+
+ $maps = array();
+
+ /* Make sure the topic starter is in the map */
+ $maps[ $topicRow['USER_ID'] ] = array(
+ 'map_user_id' => $topicRow['USER_ID'],
+ 'map_read_time' => \IPS\DateTime::create()->setTimestamp( $topicRow['TOPIC_TIME'] )
+ );
+
+ foreach ( $this->db->select( '*', 'PRIVATE_MESSAGE_USERS', array( 'TOPIC_ID=?', $topicRow['TOPIC_ID'] ) ) as $userRow )
+ {
+ $maps[ $userRow['USER_ID'] ] = array(
+ 'map_user_id' => $userRow['USER_ID'],
+ 'map_read_time' => $userRow['MESSAGE_LAST_READ']
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+ $libraryClass->setLastKeyValue( $topicRow['TOPIC_ID'] );
+ }
+ }
+
+ /**
+ * Convert private message replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'POST_ID' );
+
+ foreach( $this->fetch( 'PRIVATE_MESSAGE_POSTS', 'POST_ID' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['POST_ID'],
+ 'msg_topic_id' => $row['TOPIC_ID'],
+ 'msg_date' => \IPS\DateTime::create()->setTimestamp( $row['POST_TIME'] ),
+ 'msg_post' => $row['POST_DEFAULT_BODY'],
+ 'msg_author_id' => $row['USER_ID'],
+ 'msg_ip_address' => '127.0.0.1',
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['POST_ID'] );
+ }
+ }
+
+ /**
+ * Convert profanity filters
+ *
+ * @return void
+ */
+ public function convertProfanityFilters()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach ( $this->db->select( '*', 'CENSOR_LIST', static::$profanityFiltersWhere ) as $row )
+ {
+ /**
+ * UBB seems to support regex based profanity filters to some extent. We want to avoid converting those,
+ * but if there is a filter that ends with with a wildcard match, we can convert it to a non-exact profanity
+ * filter
+ */
+ $parsedWord = str_replace( '(.*?)', '', $row['CENSOR_WORD'] );
+ $exact = ( $parsedWord == $row['CENSOR_WORD'] );
+
+ $libraryClass->convertProfanityFilter( array(
+ 'wid' => $row['CENSOR_WORD'],
+ 'type' => $parsedWord,
+ 'swop' => $row['CENSOR_REPLACE_WITH'],
+ 'm_exact' => $exact
+ ) );
+ }
+
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ /* Make sure it's a UBBThreads URL */
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'ubbthreads.php' ) === FALSE )
+ {
+ return NULL;
+ }
+
+ if( preg_match( '#/ubbthreads.php/users/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ $data = $this->app->getLink( (int) $matches[1], array( 'members', 'core_members' ) );
+ $item = \IPS\Member::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Vanilla.php b/applications/convert/sources/Software/Core/Vanilla.php
new file mode 100644
index 00000000..8114c31e
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Vanilla.php
@@ -0,0 +1,522 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2015 Invision Power Services, Inc.
+ * @package IPS Social Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vanilla extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Vanilla 2";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vanilla";
+ }
+
+ /**
+ * @brief The activity type ID for status updates
+ */
+ protected static $activityStatusType = NULL;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app )
+ {
+ $return = parent::__construct( $app );
+
+ /* What is the activity type ID for status updates? */
+ if ( static::$activityStatusType === NULL )
+ {
+ try
+ {
+ static::$activityStatusType = $this->db->select( 'ActivityTypeID', 'ActivityType',
+ array( 'Name=?', 'Status' ), NULL, 1
+ )->first();
+ }
+ catch ( \Exception $e ) {}
+ }
+
+ return $return;
+ }
+
+ /**
+ * Resolve the filesystem location of a file from a path stored in the database
+ * There are several different ways Vanilla may store these, including as remote URL's
+ *
+ * @param $location File location retrieved from the database
+ * @param $uploadsPath Configured uploads path
+ * @return \IPS\Http\Url|string|void
+ */
+ public static function parseMediaLocation( $location, $uploadsPath )
+ {
+ $uploadsPath = str_replace( '\\', '/', $uploadsPath );
+
+ // URL
+ if ( preg_match( '`^https?://`', $location ) )
+ {
+ return \IPS\Http\Url::external( $location );
+ }
+ // Full filesystem path
+ elseif ( mb_strpos( $location, $uploadsPath ) === 0 )
+ {
+ return $location;
+ }
+ // Deprecated "plugin based" path
+ elseif ( preg_match( '`^~([^/]*)/(.*)$`', $location, $matches ) )
+ {
+ return $uploadsPath.'/'.$matches[2];
+ }
+ else
+ {
+ $parts = parse_url( $location );
+ if ( empty( $parts['scheme'] ) )
+ {
+ return $uploadsPath.'/'.$location;
+ }
+ else
+ {
+ if ( !isset( $parts['path'], $parts['host'] ) )
+ {
+ return;
+ }
+
+ // This is a url in the format type:://domain/path.
+ $result = array(
+ 'name' => ltrim( $parts['path'], '/'),
+ 'type' => $parts['scheme'],
+ 'domain' => $parts['host']
+ );
+
+ // @TODO: This is deprecated, and I'm not sure what it was for.
+ #$format = "{$result['type']}://{$result['domain']}/%s";
+ #return sprintf( $format, $result['name'] );
+ }
+ }
+ }
+
+ /**
+ * Attempt to convert a MySQL datetime string into a unix timestamp
+ *
+ * @param string $date Date(Time) string
+ * @return int|null Timestamp on successful conversion, otherwise NULL
+ */
+ public static function mysqlToDateTime( $date )
+ {
+ return \IPS\DateTime::ts( strtotime( $date ) ?: time() );
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGroups' => array(
+ 'table' => 'Role',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'User',
+ 'where' => NULL,
+ ),
+ 'convertRemoteAvatars' => array(
+ 'table' => NULL,
+ 'where' => NULL,
+ ),
+ 'convertStatuses' => array(
+ 'table' => 'Activity',
+ 'where' => array( 'ActivityTypeId=?', (int) static::$activityStatusType )
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'Conversation',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'ConversationMessage',
+ 'where' => NULL,
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_vanilla_preconvert';
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGroups',
+ 'convertMembers'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ switch( $method )
+ {
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[ $group->g_id ] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'Role' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['RoleID']}"] = $group['Name'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['RoleID']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['RoleID']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['attach_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'attach_location' );
+ $return['convertMembers']['attach_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_vanilla_photopath'),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return string Message to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'core_member_status_updates', 'class' => 'IPS\core\Statuses\Status' ), 2, array( 'app', 'link', 'class' ) );
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ /* Attachments */
+ \IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_rebuild_attachments" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return $post;
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'r.RoleID' );
+
+ /* Ready Roles */
+ $roles = $this->fetch( array( 'Role', 'r' ), 'RoleID' );
+ //$roles->join( array( 'Permission', 'p' ), 'r.RoleID=p.PermissionID' );
+
+ /* Run conversions */
+ foreach ( $roles as $row )
+ {
+ $info = array(
+ 'g_id' => $row['RoleID'],
+ 'g_name' => $row['Name'],
+ );
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['RoleID']}"] != 'none' )
+ ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['RoleID']}"]
+ : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+ $libraryClass->setLastKeyValue( $row['RoleID'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'u.UserID' );
+
+ $uploadsPath = $this->app->_session['more_info']['convertMembers']['attach_location'];
+
+ $users = $this->fetch( array( 'User', 'u' ), 'u.UserID', array( "u.Deleted<>?", 1 ) );
+ $users->join( array( 'UserRole', 'ur' ), 'ur.UserID=u.UserID' );
+ foreach( $users AS $row )
+ {
+ /* Work out birthday */
+ $bdayDay = NULL;
+ $bdayMonth = NULL;
+ $bdayYear = NULL;
+
+ $dateOfBirth = !empty( $row['DateOfBirth'] ) ? @\IPS\DateTime::ts( strtotime( $row['DateOfBirth'] ) ) : NULL;
+ if ( $dateOfBirth instanceof \IPS\DateTime )
+ {
+ $bdayYear = $dateOfBirth->format( 'Y' );
+ $bdayMonth = $dateOfBirth->format( 'n' );
+ $bdayDay = $dateOfBirth->format( 'j' );
+ }
+
+ /* Work out banned stuff */
+ $tempBan = ( $row['Banned'] == 1 ) ? -1 : 0;
+
+ /* Array of basic data */
+ $info = array(
+ 'member_id' => $row['UserID'],
+ 'email' => $row['Email'],
+ 'name' => $row['Name'],
+ 'password' => $row['Password'],
+ 'member_group_id' => $row['RoleID'] ?: NULL,
+ 'joined' => static::mysqlToDateTime( $row['DateInserted'] ),
+ 'ip_address' => $row['InsertIPAddress'],
+ 'temp_ban' => $tempBan,
+ 'bday_day' => $bdayDay,
+ 'bday_month' => $bdayMonth,
+ 'bday_year' => $bdayYear,
+ 'msg_count_new' => (int) $row['CountUnreadDiscussions'],
+ 'msg_count_total' => (int) $row['CountDiscussions'],
+ 'last_visit' => static::mysqlToDateTime( $row['DateLastActive'] ),
+ 'timezone' => !empty( $row['HourOffset'] )
+ ? (int) $row['HourOffset']
+ : new \DateTimeZone(
+ 'UTC'
+ ),
+ 'member_posts' => (int) $row['CountComments'],
+ );
+
+ /* Profile Photos */
+ $filepath = NULL;
+ $filename = NULL;
+
+ if ( !empty( $row['Photo'] ) AND ( $location = static::parseMediaLocation( $row['Photo'], $uploadsPath ) ) )
+ {
+ if ( $location instanceof \IPS\Http\Url )
+ {
+ /* The library uses file_get_contents() so we can just pop the file name off and pass the URL directly */
+ $filebits = explode( '/', (string) $location );
+ $filename = array_pop( $filebits );
+ $filepath = implode( '/', $filebits );
+ }
+ else
+ {
+ // Full-sized profile photos start with "p", small profile photos start with "n"
+ $filename = 'p' . pathinfo( $location, \PATHINFO_BASENAME );
+ $filepath = pathinfo( $location, \PATHINFO_DIRNAME );
+ }
+
+ }
+
+ /* Finally */
+ $libraryClass->convertMember( $info, NULL, $filename, $filepath );
+ $libraryClass->setLastKeyValue( $row['UserID'] );
+ }
+ }
+
+ /**
+ * Convert statuses
+ *
+ * @return void
+ */
+ public function convertStatuses()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'ActivityID' );
+
+ $statuses = $this->fetch( 'Activity', 'ActivityID', array( 'ActivityTypeID=?', static::$activityStatusType ) );
+
+ /* Run conversions */
+ foreach ( $statuses as $row )
+ {
+ $info = array(
+ 'status_id' => $row['ActivityID'],
+ 'status_member_id' => $row['ActivityUserID'],
+ 'status_date' => \IPS\DateTime::ts( strtotime( $row['DateInserted'] ) ?: time() ),
+ 'status_content' => $row['Story'],
+ //'status_replies' => $this->countRows( 'ActivityComment', array( 'ActivityID=?', $row['ActivityID'] ) ),
+ 'status_author_id' => $row['ActivityUserID'],
+ 'status_author_ip' => $row['InsertIPAddress'],
+ );
+
+ $libraryClass->convertStatus( $info );
+ $libraryClass->setLastKeyValue( $row['ActivityID'] );
+ }
+ }
+
+ /**
+ * Convert PMs
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'ConversationID' );
+
+ $messages = $this->fetch( 'Conversation', 'ConversationID' );
+
+ /* Run conversions */
+ foreach ( $messages as $row )
+ {
+ /* Message topic information */
+ //$firstMessageId = $row['FirstMessageID'];
+ $topicInfo = array(
+ 'mt_id' => $row['ConversationID'],
+ 'mt_title' => $row['Subject'] ?: NULL,
+ 'mt_date' => static::mysqlToDateTime( $row['DateInserted'] ),
+ 'mt_starter_id' => $row['InsertUserID'],
+ 'mt_last_post_time' => static::mysqlToDateTime( $row['DateUpdated'] ),
+ 'mt_to_count' => $row['CountParticipants'],
+ 'mt_replies' => $row['CountMessages'],
+ );
+
+ /* Message maps */
+ $maps = array();
+ $rows = $this->fetch( 'UserConversation', 'UserID', array( 'ConversationID=?', $row['ConversationID'] ) );
+
+ foreach ( $rows as $row )
+ {
+ $maps[ $row['UserID'] ] = array(
+ 'map_user_id' => $row['UserID'],
+ 'map_read_time' => static::mysqlToDateTime( $row['DateLastViewed'] ),
+ //'map_folder_id' => TODO: Create a Bookmarks folder for "Bookmarked" conversations?
+ 'map_user_active' => (int) ( $row['Deleted'] != '0' ),
+ 'map_last_topic_reply' => static::mysqlToDateTime( $row['DateConversationUpdated'] )
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topicInfo, $maps );
+ $libraryClass->setLastKeyValue( $row['MessageID'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'MessageID' );
+
+ foreach( $this->fetch( 'ConversationMessage', 'MessageID' ) AS $row )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['MessageID'],
+ 'msg_topic_id' => $row['ConversationID'],
+ 'msg_date' => static::mysqlToDateTime( $row['DateInserted'] ),
+ 'msg_post' => $row['Body'],
+ 'msg_author_id' => $row['InsertUserID'],
+ 'msg_ip_address' => $row['InsertIPAddress']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['MessageID'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Vbulletin.php b/applications/convert/sources/Software/Core/Vbulletin.php
new file mode 100644
index 00000000..fcc47f6d
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Vbulletin.php
@@ -0,0 +1,1747 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * @brief vBulletin 4 Stores all attachments under one table - this will store the content type for the forums app.
+ */
+ protected static $postContentType = NULL;
+
+ /**
+ * @brief The schematic for vB3 and vB4 is similar enough that we can make specific concessions in a single converter for either version.
+ */
+ protected static $isLegacy = NULL;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @param bool Establish a DB connection
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app, $needDB=TRUE )
+ {
+ $return = parent::__construct( $app, $needDB );
+
+ if ( $needDB )
+ {
+ try
+ {
+ /* Is this vB3 or vB4? */
+ if ( static::$isLegacy === NULL )
+ {
+ $version = $this->db->select( 'value', 'setting', array( "varname=?", 'templateversion' ) )->first();
+
+ if ( mb_substr( $version, 0, 1 ) == '3' )
+ {
+ static::$isLegacy = TRUE;
+ }
+ else
+ {
+ static::$isLegacy = FALSE;
+ }
+ }
+
+
+ /* If this is vB4, what is the content type ID for posts? */
+ if ( static::$postContentType === NULL AND ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) )
+ {
+ static::$postContentType = $this->db->select( 'contenttypeid', 'contenttype', array( "class=?", 'Post' ) )->first();
+ }
+ }
+ catch( \Exception $e ) {} # If we can't query, we won't be able to do anything anyway
+ }
+
+ return $return;
+ }
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin (3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertEmoticons' => array(
+ 'table' => 'smilie',
+ 'where' => NULL
+ ),
+ 'convertCustomBbcode' => array(
+ 'table' => 'bbcode',
+ 'where' => NULL
+ ),
+ 'convertProfileFieldGroups' => array(
+ 'table' => 'profilefieldcategory',
+ 'where' => NULL
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'profilefield',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'usergroup',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'user',
+ 'where' => NULL
+ ),
+ 'convertStatuses' => array(
+ 'table' => 'visitormessage',
+ 'where' => NULL
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'userlist',
+ 'where' => array( "type=?", 'ignore' )
+ ),
+ 'convertAnnouncements' => array(
+ 'table' => 'announcement',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'pm',
+ 'where' => array( "parentpmid=?", 0 ),
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'pm',
+ 'where' => array( 'folderid!=?', -1 )
+ ),
+ 'convertRanks' => array(
+ 'table' => 'usertitle',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Can we convert settings?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Settings Map
+ *
+ * @return array
+ */
+ public function settingsMap()
+ {
+ return array(
+ 'bbtitle' => 'board_name',
+ );
+ }
+
+ /**
+ * Settings Map Listing
+ *
+ * @return array
+ */
+ public function settingsMapList()
+ {
+ $settings = array();
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ try
+ {
+ $setting = $this->db->select( 'varname, value', 'setting', array( "varname=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ try
+ {
+ $title = $this->db->select( 'text', 'phrase', array( "varname=?", "setting_{$setting['varname']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $title = $setting['varname'];
+ }
+
+ $settings[$setting['varname']] = array( 'title' => $title, 'value' => $setting['value'], 'our_key' => $ours, 'our_title' => \IPS\Member::loggedIn()->language()->addToStack( $ours ) );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_vb4_preconvert';
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertProfileFieldGroups',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers',
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ $return['convertEmoticons'] = array(
+ 'emoticon_path' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_vb_smilie_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ 'keep_existing_emoticons' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ )
+ );
+ break;
+
+ case 'convertProfileFieldGroups':
+ $return['convertProfileFieldGroups'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_groups' ), 'IPS\core\ProfileFields\Group' ) AS $group )
+ {
+ $options[$group->_id] = $group->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profilefieldcategory' ) AS $group )
+ {
+ $id = $group['profilefieldcategoryid']; # vB doesn't use spaces in column names and its driving me crazy typing it out
+
+ try
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}"] = $this->db->select( 'text', 'phrase', array( "varname=?", "category{$id}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}"] = "vBulletin Profile Group {$id}";
+ }
+
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfgroup_desc' );
+
+ $return['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profilefield' ) AS $field )
+ {
+ try
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}"] = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}"] = "vBulletin Profile Field {$field['profilefieldid']}";
+ }
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'usergroup' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['usergroupid']}"] = $group['title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['usergroupid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['usergroupid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* We can only retain one type of photo */
+ $return['convertMembers']['photo_type'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'avatars',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'avatars' => \IPS\Member::loggedIn()->language()->addToStack( 'avatars' ), 'profile_photos' => \IPS\Member::loggedIn()->language()->addToStack( 'profile_photos' ) ) ),
+ 'field_hint' => NULL,
+ );
+
+ /* Find out where the photos live */
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( $value != 'database' AND !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ /* And decide what to do about these... */
+ foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'msn', 'skype' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => ucwords( $field ) ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'core_member_status_updates', 'class' => 'IPS\core\Statuses\Status' ), 2, array( 'app', 'link', 'class' ) );
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_announcements', 'extension' => 'core_Announcement' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ /* Attachments */
+ \IPS\Task::queue( 'core', 'RebuildAttachmentThumbnails', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild", "f_announce_rebuild" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ $post = preg_replace( "#\[sigpic\](.+?)\[\/sigpic\]#i", "", $post );
+ $post = str_replace( "<", "<", $post );
+ $post = str_replace( ">", ">", $post );
+ $post = str_replace( "'", "'", $post );
+
+ $post = nl2br( $post );
+
+ $post = preg_replace( "#\[quote=([^\];]+?)\]#i", "[quote name='$1']", $post );
+ $post = preg_replace( "#\[quote=([^\];]+?);\d+\]#i", "[quote name='$1']", $post );
+ $post = preg_replace( "#\[quote=([^\];]+?);n\d+\]#i", "[quote name='$1']", $post );
+
+ /* Remove video tags and allow our parser to handle the embeds it supports */
+ $post = preg_replace( "#\[video=[a-z]+;[a-z0-9_]+\](.*?)\[\/video\]#i", '$1', $post );
+
+ /* This is not a core feature in vBulletin, but is a popular addon and the code was supplied below.
+ Addon URL: https://www.dragonbyte-tech.com/store/product/20-advanced-user-tagging/ */
+ preg_match_all( '#\[MENTION=(\d+)\](.+?)\[\/MENTION\]#i', $post, $matches );
+
+ if ( count( $matches ) )
+ {
+ if ( isset( $matches[1] ) )
+ {
+ $mentions = array();
+ foreach( $matches[1] AS $k => $v )
+ {
+ if ( isset( $matches[2][$k] ) )
+ {
+ $mentions[ $matches[2][$k] ] = $v;
+ }
+ }
+
+ $maps = array();
+ $urls = array();
+ $cardUrls = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'name', array_keys( $mentions ) ) ) ), 'IPS\Member' ) AS $member )
+ {
+ $maps[$member->name] = $member->member_id;
+ $urls[$member->name] = $member->url();
+ $cardUrls[$member->name] = $member->url()->setQueryString( 'do', 'hovercard' );
+ }
+
+ foreach( $mentions AS $member_name => $member_id )
+ {
+ $maps[$member_name] = preg_quote( $maps[$member_name], '#' );
+ $member_name = preg_quote( $member_name, '#' );
+
+ $post = preg_replace( "#\[MENTION={$member_id}\]{$member_name}\[\/MENTION\]#i", "@{$member_name} ", $post );
+ }
+ }
+ }
+
+ return $post;
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'smilieid' );
+
+ foreach( $this->fetch( 'smilie', 'smilieid' ) AS $emoticon )
+ {
+ $filename = explode( '/', $emoticon['smiliepath'] );
+ $filename = array_pop( $filename );
+
+ $info = array(
+ 'id' => $emoticon['smilieid'],
+ 'typed' => $emoticon['smilietext'],
+ 'filename' => $filename,
+ 'emo_position' => $emoticon['displayorder'],
+ );
+
+ try
+ {
+ $imageCategory = $this->db->select( '*', 'imagecategory', array( "imagecategoryid=?", $emoticon['imagecategoryid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $imageCategory = array(
+ 'title' => "Converted",
+ 'displayorder' => 1,
+ );
+ }
+
+ $set = array(
+ 'set' => md5( $imageCategory['title'] ),
+ 'title' => $imageCategory['title'],
+ 'position' => $imageCategory['displayorder']
+ );
+
+ $libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $this->app->_session['more_info']['convertEmoticons']['emoticon_path'] );
+
+ $libraryClass->setLastKeyValue( $emoticon['smilieid'] );
+ }
+ }
+
+ /**
+ * Convert profile field groups
+ *
+ * @return void
+ */
+ public function convertProfileFieldGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profilefieldcategoryid' );
+
+ foreach( $this->fetch( 'profilefieldcategory', 'profilefieldcategoryid' ) AS $group )
+ {
+ try
+ {
+ $name = $this->db->select( 'text', 'phrase', array( "varname=?", "category{$group['profilefieldcategoryid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $name = "vBulletin Profile Group {$group['profilefieldcategoryid']}";
+ }
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] : NULL;
+
+ $libraryClass->convertProfileFieldGroup( array(
+ 'pf_group_id' => $group['profilefieldcategoryid'],
+ 'pf_group_name' => $name,
+ 'pf_group_order' => $group['displayorder']
+ ), $merge );
+
+ $libraryClass->setLastKeyValue( $group['profilefieldcategoryid'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profilefieldid' );
+
+ foreach( $this->fetch( 'profilefield', 'profilefieldid' ) AS $field )
+ {
+ try
+ {
+ $name = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $name = "vBulletin Profile Field {$field['profilefieldid']}";
+ }
+
+ try
+ {
+ $desc = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_desc" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $desc = "";
+ }
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] : NULL;
+
+ if ( in_array( mb_strtolower( $field['type'] ), array( 'select', 'radio', 'checkbox', 'select_multiple' ) ) )
+ {
+ $content = json_encode( \unserialize( $field['data'] ) );
+ }
+ else
+ {
+ $content = json_encode( array() );
+ }
+
+ $type = static::_pfieldMap( $field['type'] );
+
+ $multiple = 0;
+ if ( $field['type'] == 'select_multiple' )
+ {
+ $multiple = 1;
+ }
+
+ $info = array(
+ 'pf_id' => $field['profilefieldid'],
+ 'pf_name' => $name,
+ 'pf_desc' => $desc,
+ 'pf_type' => $type,
+ 'pf_content' => $content,
+ 'pf_not_null' => ( in_array( $field['required'], array( 1, 3 ) ) ) ? 1 : 0,
+ 'pf_member_hide' => $field['hidden'],
+ 'pf_max_input' => $field['maxlength'],
+ 'pf_member_edit' => ( $field['editable'] >= 1 ) ? 1 : 0,
+ 'pf_position' => $field['displayorder'],
+ 'pf_show_on_reg' => ( $field['required'] == 2 ) ? 1 : 0,
+ 'pf_input_format' => $field['regex'],
+ 'pf_admin_only' => ( $field['editable'] == 0 ) ? 1 : 0,
+ 'pf_group_id' => $field['profilefieldcategoryid'],
+ 'pf_multiple' => $multiple
+ );
+
+ $libraryClass->convertProfileField( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $field['profilefieldid'] );
+ }
+ }
+
+ /**
+ * Maps vBulletin Profile Field type to the IPS Equivalent
+ *
+ * @param string The vB Field Type
+ * @return string The IPS Field Type
+ */
+ protected static function _pfieldMap( $type )
+ {
+ switch( mb_strtolower( $type ) )
+ {
+ case 'select':
+ case 'radio':
+ case 'checkbox':
+ return ucwords( $type );
+ break;
+
+ case 'textarea':
+ return 'TextArea';
+ break;
+
+ case 'select_multiple':
+ return 'Select';
+ break;
+
+ /* Just do Text as default */
+ default:
+ return 'Text';
+ break;
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'usergroupid' );
+
+ foreach( $this->fetch( 'usergroup', 'usergroupid' ) AS $group )
+ {
+ /* <3 Closures */
+ $self = $this;
+ $checkpermission = function ( $name, $perm ) use ( $group, $self )
+ {
+ if ( $group[$name] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ };
+
+ /* Work out promotion */
+ $g_promotion = '-1&-1';
+ $gbw_promote_unit = 0;
+ try
+ {
+ $promotion = $this->db->select( '*', 'userpromotion', array( "usergroupid=?", $group['usergroupid'] ) )->first();
+
+ /* We only support Posts or Join Date */
+ if ( in_array( $promotion['strategy'], array( 17, 18 ) ) )
+ {
+ switch( $promotion['strategy'] )
+ {
+ case 17: # posts
+ $g_promotion = array( $promotion['joinusergroupid'], $promotion['posts'] );
+ break;
+
+ case 18: #joindate
+ $g_promotion = array( $promotion['joinusergroupid'], $promotion['date'] );
+ $gbw_promote_unit = 1;
+ break;
+ }
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Work out photo vars - vBulletin has a concept of avatars and profile photos, so we are just going to use the larger of the two */
+ $g_max_photo_vars = array();
+ $g_max_photo_vars[] = ( $group['profilepicmaxsize'] > $group['avatarmaxsize'] ) ? $group['profilepicmaxsize'] : $group['avatarmaxsize'];
+
+ /* We don't have individual controls for height and width, so work out which value is the largest and use that */
+ $highestValue = 0;
+ foreach( array( 'profilepicmaxheight', 'avatarmaxheight', 'profilepicmaxwidth', 'avatarmaxwidth' ) AS $value )
+ {
+ if ( $group[$value] > $highestValue )
+ {
+ $highestValue = $group[$value];
+ }
+ }
+
+ $g_max_photo_vars[] = $highestValue;
+ $g_max_photo_vars[] = $highestValue;
+
+ /* Work out signature limits */
+ $g_signature_limits = '1:::::';
+ if ( $checkpermission( 'genericpermissions', 'canusesignature' ) )
+ {
+ $g_signature_limits = array();
+ $g_signature_limits[] = 0;
+ $g_signature_limits[] = $group['sigmaximages'];
+ $g_signature_limits[] = $group['sigpicmaxwidth'];
+ $g_signature_limits[] = $group['sigpicmaxheight'];
+ $g_signature_limits[] = '';
+ $g_signature_limits[] = ( $group['sigmaxlines'] > 0 ) ? $group['sigmaxlines'] : '';
+ }
+
+ /* Let's disect all of these bit options */
+ $info = array(
+ 'g_id' => $group['usergroupid'],
+ 'g_name' => $group['title'],
+ 'g_view_board' => $checkpermission( 'forumpermissions', 'canview' ) ? 1 : 0,
+ 'g_mem_info' => $checkpermission( 'genericpermissions', 'canviewmembers' ) ? 1 : 0,
+ 'g_use_search' => $checkpermission( 'forumpermissions', 'cansearch' ) ? 1 : 0,
+ 'g_edit_profile' => $checkpermission( 'genericpermissions', 'canmodifyprofile' ) ? 1 : 0,
+ 'g_edit_posts' => $checkpermission( 'forumpermissions', 'caneditpost' ) ? 1 : 0,
+ 'g_delete_own_posts' => $checkpermission( 'forumpermissions', 'candeletepost' ) ? 1 : 0,
+ 'g_use_pm' => ( $group['pmquota'] > 0 ) ? 1 : 0,
+ 'g_is_supmod' => $checkpermission( 'adminpermissions', 'ismoderator' ) ? 1 : 0,
+ 'g_access_cp' => $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ? 1 : 0,
+ 'g_append_edit' => $checkpermission( 'genericoptions', 'showeditedby' ) ? 1 : 0, # Fun fact, I couldn't find this as it was actually in the place it should be rather than forumpermissions
+ 'g_access_offline' => $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ? 1 : 0,
+ 'g_attach_max' => ( $group['attachlimit'] > 0 ) ? $group['attachlimit'] : -1,
+ 'prefix' => $group['opentag'],
+ 'suffix' => $group['closetag'],
+ 'g_max_messages' => $group['pmquota'],
+ 'g_max_mass_pm' => $group['pmsendmax'],
+ 'g_promotion' => $g_promotion,
+ 'g_photo_max_vars' => $g_max_photo_vars,
+ 'g_bypass_badwords' => ( ( $checkpermission( 'adminpermissions', 'ismoderator' ) OR $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ) AND $this->_setting( 'ctCensorMod' ) ),
+ 'g_mod_preview' => !$checkpermission( 'forumpermissions', 'followforummoderation' ) ? 1 : 0,
+ 'g_signature_limits' => $g_signature_limits,
+ 'g_bitoptions' => array(
+ 'gbw_promote_unit_type' => $gbw_promote_unit, // Type of group promotion to use. 1 is days since joining, 0 is content count. Corresponds to g_promotion
+ 'gbw_no_status_update' => !$checkpermission( 'visitormessagepermissions', 'canmessageownprofile' ), // Can NOT post status updates
+ 'gbw_soft_delete_own' => $checkpermission( 'forumpermissions', 'candeletepost' ), // Allow users of this group to hide their own submitted content
+ 'gbw_allow_upload_bgimage' => 1, // Can upload a cover photo?
+ 'gbw_view_reps' => $checkpermission( 'genericpermissions2', 'canprofilerep' ), // Can view who gave reputation?
+ 'gbw_no_status_import' => !$checkpermission( 'visitormessagepermissions', 'canmessageownprofile' ), // Can NOT import status updates from Facebook/Twitter
+ 'gbw_disable_tagging' => !$checkpermission( 'genericpermissions', 'cancreatetag' ), // Can NOT use tags
+ 'gbw_disable_prefixes' => !$checkpermission( 'genericpermissions', 'cancreatetag' ), // Can NOT use prefixes
+ 'gbw_pm_override_inbox_full'=> $checkpermission( 'pmpermissions', 'canignorequota' ), // 1 means this group can send other members PMs even when that member's inbox is full
+ 'gbw_cannot_be_ignored' => ( ( $checkpermission( 'adminpermissions', 'ismoderator' ) OR $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ) AND $this->_setting( 'ignoremods' ) ), // 1 means they cannot be ignored. 0 means they can
+ ),
+ 'g_pm_flood_mins' => $this->_setting( 'pmfloodtime' ) / 60,
+ 'g_post_polls' => $checkpermission( 'forumpermissions', 'canpostpoll' ) ? 1 : 0,
+ 'g_vote_polls' => $checkpermission( 'forumpermissions', 'canvote' ) ? 1 : 0,
+ 'g_topic_rate_setting' => $checkpermission( 'forumpermissions', 'canthreadrate' ) ? 1 : 0
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$group['usergroupid']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$group['usergroupid']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $group['usergroupid'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'user.userid' );
+
+ foreach( $this->fetch( 'user', 'user.userid', NULL, 'user.*, usertextfield.signature' )->join( 'usertextfield', 'user.userid = usertextfield.userid' ) AS $user )
+ {
+ /* <3 Closures */
+ $self = $this;
+ $checkpermission = function ( $name, $perm ) use ( $user, $self )
+ {
+ if ( $name == 'useroptions' )
+ {
+ $key = 'options';
+ }
+ else
+ {
+ $key = $name;
+ }
+
+ if ( $user[$key] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ };
+
+ /* Fetch our last warning */
+ $warn_lastwarn = 0;
+ try
+ {
+ $warn_lastwarn = $this->db->select( 'dateline', 'infraction', array( "userid=?", $user['userid'] ), "dateline DESC", 1 )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Birthday */
+ if ( $user['birthday'] )
+ {
+ list( $bday_month, $bday_day, $bday_year ) = explode( '-', $user['birthday'] );
+
+ if ( $bday_year == '0000' )
+ {
+ $bday_year = NULL;
+ }
+ }
+ else
+ {
+ $bday_month = $bday_day = $bday_year = NULL;
+ }
+
+ /* Auto Track */
+ $auto_track = 0;
+ switch( $user['autosubscribe'] )
+ {
+ case 1:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'immediate' );
+ break;
+
+ case 2:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'daily' );
+ break;
+
+ case 3:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'weekly' );
+ break;
+ }
+
+ /* User Banned */
+ $temp_ban = 0;
+ try
+ {
+ $temp_ban = $this->db->select( 'liftdate', 'userban', array( "userid=?", $user['userid'] ) )->first();
+
+ if ( $temp_ban == 0 )
+ {
+ $temp_ban = -1;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Main Members Table */
+ $info = array(
+ 'member_id' => $user['userid'],
+ 'member_group_id' => $user['usergroupid'],
+ 'mgroup_others' => $user['membergroupids'],
+ 'name' => $user['username'],
+ 'email' => $user['email'],
+ 'member_title' => $user['usertitle'],
+ 'joined' => $user['joindate'],
+ 'ip_address' => $user['ipaddress'],
+ 'warn_level' => ( $user['ipoints'] > 2147483647 ) ? 2147483647 : $user['ipoints'],
+ 'warn_lastwarn' => $warn_lastwarn,
+ 'bday_day' => $bday_day,
+ 'bday_month' => $bday_month,
+ 'bday_year' => $bday_year,
+ 'last_visit' => $user['lastvisit'],
+ 'last_activity' => $user['lastactivity'],
+ 'auto_track' => $auto_track,
+ 'temp_ban' => $temp_ban,
+ 'members_profile_views' => $user['profilevisits'],
+ 'conv_password' => $user['password'],
+ 'conv_password_extra' => $user['salt'],
+ 'members_bitoptions' => array(
+ 'view_sigs' => $checkpermission( 'useroptions', 'showsignatures' ), // View signatures?
+ 'coppa_user' => $checkpermission( 'useroptions', 'coppauser' ), // Was the member validated using coppa?
+ 'timezone_override' => !$checkpermission( 'useroptions', 'dstauto' ), // If TRUE, user's timezone will not be detected automatically
+ ),
+ 'members_bitoptions2' => array(
+ 'show_pm_popup' => $user['pmpopup'], // "Show pop-up when I have a new message"
+ ),
+ 'pp_setting_count_comments' => $checkpermission( 'useroptions', 'vm_enable' ),
+ 'pp_reputation_points' => $user['reputation'],
+ 'signature' => $user['signature'] ?: '',
+ 'allow_admin_mails' => $checkpermission( 'useroptions', 'adminemail' ),
+ 'members_disable_pm' => !$checkpermission( 'useroptions', 'receivepm' ),
+ 'member_posts' => $user['posts'],
+ );
+
+ /* Profile Fields */
+ try
+ {
+ $profileFields = $this->db->select( '*', 'userfield', array( "userid=?", $user['userid'] ) )->first();
+
+ unset( $profileFields['userid'] );
+ unset( $profileFields['temp'] );
+
+ /* Basic fields - we only need ID => Value, the library will handle the rest */
+ foreach( $profileFields AS $key => $value )
+ {
+ $profileFields[ str_replace( 'field', '', $key ) ] = $value;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $profileFields = array();
+ }
+
+ /* Pseudo Fields */
+ foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'msn', 'skype' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $profileFields[$pseudo] = $user[$pseudo];
+ }
+
+ /* Profile Photos */
+ $firstTable = 'customavatar';
+ $secondTable = 'customprofilepic';
+ if ( $this->app->_session['more_info']['convertMembers']['photo_type'] == 'profile_photos' )
+ {
+ $firstTable = 'customprofilepic';
+ $secondTable = 'customavatar';
+ }
+ if ( $this->app->_session['more_info']['convertMembers']['photo_location'] == 'database' )
+ {
+ try
+ {
+ foreach( $this->db->select( 'filedata, filename', $firstTable, array( "userid=?", $user['userid'] ) )->first() AS $key => $value )
+ {
+ if ( $key == 'filedata' )
+ {
+ $filedata = $value;
+ }
+ else
+ {
+ $filename = $value;
+ }
+ }
+ $filepath = NULL;
+ }
+ catch( \UnderflowException $e )
+ {
+ try
+ {
+ foreach( $this->db->select( 'filedata, filename', $secondTable, array( "userid=?", $user['userid'] ) )->first() AS $key => $value )
+ {
+ if ( $key == 'filedata' )
+ {
+ $filedata = $value;
+ }
+ else
+ {
+ $filename = $value;
+ }
+ }
+ $filepath = NULL;
+ }
+ catch( \UnderflowException $e )
+ {
+ list( $filedata, $filename, $filepath ) = array( NULL, NULL, NULL );
+ }
+ }
+ }
+ else
+ {
+ $filepath = $this->app->_session['more_info']['convertMembers']['photo_location'];
+ $first = 'avatar';
+ $second = 'profilepic';
+
+ if ( $this->app->_session['more_info']['convertMembers']['photo_type'] == 'profile_photos' )
+ {
+ $first = 'profilepic';
+ $second = 'avatar';
+ }
+
+ try
+ {
+ try
+ {
+ $ext = $this->db->select( 'filename', $firstTable, array( "userid=?", $user['userid'] ) )->first();
+ $ext = explode( '.', $ext );
+ $ext = array_pop( $ext );
+
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.'. $ext ) )
+ {
+ $filename = $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.'. $ext;
+ }
+ else
+ {
+ /* The filename with the original extension doesn't exist. vBulletin may storing the image as a .gif file instead, so try that. */
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.gif' ) )
+ {
+ $filename = $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.gif';
+ }
+ else
+ {
+ /* Throw an exception so we can try the other */
+ throw new \UnderflowException;
+ }
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $ext = $this->db->select( 'filename', $secondTable, array( "userid=?", $user['userid'] ) )->first();
+ $ext = explode( '.', $ext );
+ $ext = array_pop( $ext );
+
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.'. $ext ) )
+ {
+ $filename = $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.'. $ext;
+ }
+ else
+ {
+ /* The filename with the original extension doesn't exist. vBulletin may be storing the image as a .gif file instead, so try that. */
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.gif' ) )
+ {
+ $filename = $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.gif';
+ }
+ else
+ {
+ throw new \UnderflowException;
+ }
+ }
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ list( $filedata, $filename, $filepath ) = array( NULL, NULL, NULL );
+ }
+ }
+
+ $libraryClass->convertMember( $info, $profileFields, $filename, $filepath, $filedata );
+
+ /* Any friends need converting to followers? */
+ foreach( $this->db->select( '*', 'userlist', array( "type=? AND friend=? AND userid=?", 'buddy', 'yes', $user['userid'] ) ) AS $follower )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'members',
+ 'follow_rel_id' => $follower['relationid'],
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $follower['userid'],
+ ) );
+ }
+
+ /* And warn logs made on the profile - we'll do content specific later */
+ foreach( $this->db->select( '*', 'infraction', array( "userid=? AND postid=?", $user['userid'], 0 ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['infractionid'],
+ 'wl_member' => $warn['userid'],
+ 'wl_moderator' => $warn['whoadded'],
+ 'wl_date' => $warn['dateline'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['note'],
+ 'wl_note_mods' => $warn['customreason'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $user['userid'] );
+ }
+ }
+
+ /**
+ * Convert status updates
+ *
+ * @return void
+ */
+ public function convertStatuses()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'vmid' );
+
+ foreach( $this->fetch( 'visitormessage', 'vmid' ) AS $status )
+ {
+ /* Work out approval status */
+ switch( $status['state'] )
+ {
+ case 'moderation':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+
+ case 'visible':
+ default:
+ $approved = 1;
+ break;
+ }
+
+ $info = array(
+ 'status_id' => $status['vmid'],
+ 'status_member_id' => $status['userid'],
+ 'status_date' => $status['dateline'],
+ 'status_content' => $status['pagetext'],
+ 'status_author_id' => $status['postuserid'],
+ 'status_author_ip' => $status['ipaddress'],
+ 'status_approved' => $approved
+ );
+
+ $libraryClass->convertStatus( $info );
+
+ $libraryClass->setLastKeyValue( $status['vmid'] );
+ }
+ }
+
+ /**
+ * Convert one or more settings
+ *
+ * @param array $settings Settings to convert
+ * @return void
+ */
+ public function convertSettings( $settings=array() )
+ {
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ if ( !isset( $values[$ours] ) OR $values[$ours] == FALSE )
+ {
+ continue;
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'value', 'setting', array( "varname=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $setting ), array( "conf_key=?", $ours ) );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'userlist', 'userid', array( "type=?", 'ignore' ) ) AS $ignore )
+ {
+ $info = array(
+ 'ignore_id' => $ignore['userid'] . '-' . $ignore['relationid'],
+ 'ignore_owner_id' => $ignore['userid'],
+ 'ignore_ignore_id' => $ignore['relationid'],
+ );
+
+ foreach( \IPS\core\Ignore::types() AS $type )
+ {
+ $info['ignore_' . $type] = 1;
+ }
+
+ $libraryClass->convertIgnoredUser( $info );
+ }
+ }
+
+ /**
+ * Convert custom bbcode
+ *
+ * @return void
+ */
+ public function convertCustomBbcode()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'bbcodeid' );
+
+ foreach( $this->fetch( 'bbcode', 'bbcodeid' ) AS $bbcode )
+ {
+ $libraryClass->convertCustomBbcode( array(
+ 'bbcode_id' => $bbcode['bbcodeid'],
+ 'bbcode_title' => $bbcode['title'],
+ 'bbcode_description' => $bbcode['bbcodeexplanation'],
+ 'bbcode_tag' => $bbcode['bbcodetag'],
+ 'bbcode_replacement' => str_replace( '%1$s', '{content}', $bbcode['bbcodereplacement'] ),
+ 'bbcode_example' => $bbcode['bbcodeexample']
+ ) );
+
+ $libraryClass->setLastKeyValue( $bbcode['bbcodeid'] );
+ }
+ }
+
+ /**
+ * Convert announcements
+ *
+ * @return void
+ */
+ public function convertAnnouncements()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'announcementid' );
+
+ foreach( $this->fetch( 'announcement', 'announcementid' ) AS $announcement )
+ {
+ $libraryClass->convertAnnouncement( array(
+ 'announce_id' => $announcement['announcementid'],
+ 'announce_title' => $announcement['title'],
+ 'announce_content' => $announcement['pagetext'],
+ 'announce_member_id' => $announcement['userid'],
+ 'announce_views' => $announcement['views'],
+ 'announce_start' => $announcement['startdate'],
+ 'announce_end' => $announcement['enddate'],
+ 'announce_active' => 0, # Set this to no - we can't really figure out where these go.
+ ) );
+
+ $libraryClass->setLastKeyValue( $announcement['announcementid'] );
+ }
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pm.pmid' );
+
+ foreach( $this->fetch( 'pm', 'pm.pmid', array( "pm.parentpmid=?", 0 ), "pm.*, pmtext.*" )->join( 'pmtext', 'pm.pmtextid = pmtext.pmtextid' ) AS $pm )
+ {
+ if ( !$pm['pmtextid'] )
+ {
+ $libraryClass->setLastKeyValue( $pm['pmid'] );
+ continue;
+ }
+
+ try
+ {
+ $replies = $this->db->select( 'COUNT(*)', 'pm', array( "parentpmid=? AND folderid!=?", $pm['pmid'], -1 ) )->first();
+ $replies += 1;
+ }
+ catch( \UnderflowException $e )
+ {
+ $replies = 1;
+ }
+
+ $topic = array(
+ 'mt_id' => $pm['pmid'],
+ 'mt_date' => $pm['dateline'],
+ 'mt_title' => $pm['title'],
+ 'mt_starter_id' => $pm['fromuserid'],
+ 'mt_start_time' => $pm['dateline'],
+ 'mt_last_post_time' => $pm['dateline'],
+ 'mt_to_count' => count( @\unserialize( $pm['touserarray'] ) ),
+ 'mt_to_member_id' => $pm['userid'],
+ 'mt_replies' => $replies,
+ 'mt_first_msg_id' => $pm['pmid'],
+ );
+
+ $authors = array();
+ $authors[ $pm['fromuserid'] ] = $pm['fromuserid'];
+
+ try
+ {
+ foreach( $this->db->select( '*', 'pm', array( "parentpmid=?", $pm['pmid'] ) )->join( 'pmtext', 'pm.pmtextid = pmtext.pmtextid' ) AS $post )
+ {
+ if ( $post['folderid'] != -1 )
+ {
+ $authors[ $post['fromuserid'] ] = $post['fromuserid'];
+ }
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ $maps = array();
+
+ foreach( $authors AS $mapAuthor )
+ {
+ $maps[ $mapAuthor ] = array(
+ 'map_user_id' => $mapAuthor,
+ 'map_topic_id' => $pm['pmid'],
+ 'map_user_active' => 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => 0,
+ 'map_is_starter' => ( $mapAuthor == $pm['fromuserid'] ) ? 1 : 0,
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $pm['pmid'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pm.pmid' );
+
+ foreach( $this->fetch( 'pm', 'pm.pmid', array( 'pm.folderid!=?', -1 ), "pm.*, pmtext.*" )->join( 'pmtext', 'pm.pmtextid = pmtext.pmtextid' ) AS $pm )
+ {
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $pm['pmid'],
+ 'msg_topic_id' => !$pm['parentpmid'] ? $pm['pmid'] : $pm['parentpmid'],
+ 'msg_date' => $pm['dateline'],
+ 'msg_post' => $pm['message'],
+ 'msg_author_id' => $pm['fromuserid'],
+ 'msg_is_first_post' => 1
+ ) );
+
+ $libraryClass->setLastKeyValue( $pm['pmid'] );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'usertitleid' );
+
+ foreach( $this->fetch( 'usertitle', 'usertitleid' ) AS $rank )
+ {
+ $libraryClass->convertRank( array(
+ 'id' => $rank['usertitleid'],
+ 'posts' => $rank['minposts'],
+ 'title' => $rank['title']
+ ) );
+
+ $libraryClass->setLastKeyValue( $rank['usertitleid'] );
+ }
+ }
+
+ /* !vBulletin Specific Stuff */
+ /**
+ * @brief Silly Bit Options for Groups. Typically we would leave out app specific options (such as Forums here) however we need them for some general permissions, like uploading.
+ * @note This is public simply because I do not want to do this ever again if it ever changes.
+ */
+ public static $bitOptions = array(
+ 'forumpermissions' => array(
+ 'canview' => 1,
+ 'canviewothers' => 2,
+ 'cansearch' => 4,
+ 'canemail' => 8,
+ 'canpostnew' => 16,
+ 'canreplyown' => 32,
+ 'canreplyothers' => 64,
+ 'caneditpost' => 128,
+ 'candeletepost' => 256,
+ 'candeletethread' => 512,
+ 'canopenclose' => 1024,
+ 'canmove' => 2048,
+ 'cangetattachment' => 4096,
+ 'canpostattachment' => 8192,
+ 'canpostpoll' => 16384,
+ 'canvote' => 32768,
+ 'canthreadrate' => 65536,
+ 'followforummoderation' => 131072,
+ 'canseedelnotice' => 262144,
+ 'canviewthreads' => 524288,
+ 'cantagown' => 1048576,
+ 'cantagothers' => 2097152,
+ 'candeletetagown' => 4194304,
+ 'canseethumbnails' => 8388608,
+ 'canattachmentcss' => 16777216,
+ 'bypassdoublepost' => 33554432,
+ 'canwrtmembers' => 67108864,
+ ),
+ 'pmpermissions' => array(
+ 'cantrackpm' => 1,
+ 'candenypmreceipts' => 2,
+ 'canignorequota' => 4,
+ ),
+ 'wolpermissions' => array(
+ 'canwhosonline' => 1,
+ 'canwhosonlineip' => 2,
+ 'canwhosonlinefull' => 4,
+ 'canwhosonlinebad' => 8,
+ 'canwhosonlinelocation' => 16,
+ ),
+ 'adminpermissions' => array(
+ 'ismoderator' => 1,
+ 'cancontrolpanel' => 2,
+ 'canadminsettings' => 4,
+ 'canadminstyles' => 8,
+ 'canadminlanguages' => 16,
+ 'canadminforums' => 32,
+ 'canadminthreads' => 64,
+ 'canadmincalendars' => 128,
+ 'canadminusers' => 256,
+ 'canadminpermissions' => 512,
+ 'canadminfaq' => 1024,
+ 'canadminimages' => 2048,
+ 'canadminbbcodes' => 4096,
+ 'canadmincron' => 8192,
+ 'canadminmaintain' => 16384,
+ 'canadminplugins' => 65536,
+ 'canadminnotices' => 131072,
+ 'canadminmodlog' => 262144,
+ 'cansitemap' => 524288,
+ 'canadminads' => 1048576,
+ 'canadmintags' => 2097152,
+ 'canadminblocks' => 4194304,
+ 'cansetdefaultprofile' => 8388608,
+ ),
+ 'genericpermissions' => array(
+ 'canviewmembers' => 1,
+ 'canmodifyprofile' => 2,
+ 'caninvisible' => 4,
+ 'canviewothersusernotes' => 8,
+ 'canmanageownusernotes' => 16,
+ 'canseehidden' => 32,
+ 'canbeusernoted' => 64,
+ 'canprofilepic' => 128,
+ 'canseeownrep' => 256,
+ 'cananimateprofilepic' => 134217728,
+ 'canuseavatar' => 512,
+ 'canusesignature' => 1024,
+ 'canusecustomtitle' => 2048,
+ 'canseeprofilepic' => 4096,
+ 'canviewownusernotes' => 8192,
+ 'canmanageothersusernotes' => 16384,
+ 'canpostownusernotes' => 32768,
+ 'canpostothersusernotes' => 65536,
+ 'caneditownusernotes' => 131072,
+ 'canseehiddencustomfields' => 262144,
+ 'canuserep' => 524288,
+ 'canhiderep' => 1048576,
+ 'cannegativerep' => 2097152,
+ 'cangiveinfraction' => 4194304,
+ 'cananimateavatar' => 67108864,
+ 'canseeinfraction' => 8388608,
+ 'cangivearbinfraction' => 536870912,
+ 'canreverseinfraction' => 16777216,
+ 'cansearchft_bool' => 33554432,
+ 'canemailmember' => 268435456,
+ 'cancreatetag' => 1073741824,
+ ),
+ 'genericpermissions2' => array(
+ 'canusefriends' => 1,
+ 'canprofilerep' => 2,
+ 'canwgomembers' => 4,
+ ),
+ 'genericoptions' => array(
+ 'showgroup' => 1,
+ 'showbirthday' => 2,
+ 'showmemberlist' => 4,
+ 'showeditedby' => 8,
+ 'allowmembergroups' => 16,
+ 'isnotbannedgroup' => 32,
+ 'requirehvcheck' => 64,
+ ),
+ 'signaturepermissions' => array(
+ 'canbbcode' => 131072,
+ 'canbbcodebasic' => 1,
+ 'canbbcodecolor' => 2,
+ 'canbbcodesize' => 4,
+ 'canbbcodefont' => 8,
+ 'canbbcodealign' => 16,
+ 'canbbcodelist' => 32,
+ 'canbbcodelink' => 64,
+ 'canbbcodecode' => 128,
+ 'canbbcodephp' => 256,
+ 'canbbcodehtml' => 512,
+ 'canbbcodequote' => 1024,
+ 'allowimg' => 2048,
+ 'allowvideo' => 262144,
+ 'allowsmilies' => 4096,
+ 'allowhtml' => 8192,
+ 'cansigpic' => 32768,
+ 'cananimatesigpic' => 65536,
+ ),
+ 'visitormessagepermissions' => array(
+ 'canmessageownprofile' => 1,
+ 'canmessageothersprofile' => 2,
+ 'caneditownmessages' => 4,
+ 'candeleteownmessages' => 8,
+ 'canmanageownprofile' => 32,
+ 'followforummoderation' => 16,
+ ),
+ 'useroptions' => array(
+ 'showsignatures' => 1,
+ 'showavatars' => 2,
+ 'showimages' => 4,
+ 'coppauser' => 8,
+ 'adminemail' => 16,
+ 'showvcard' => 32,
+ 'dstauto' => 64,
+ 'dstonoff' => 128,
+ 'showemail' => 256,
+ 'invisible' => 512,
+ 'showreputation' => 1024,
+ 'receivepm' => 2048,
+ 'emailonpm' => 4096,
+ 'hasaccessmask' => 8192,
+ 'vbasset_enable' => 16384,
+ 'postorder' => 32768,
+ 'receivepmbuddies' => 131072,
+ 'noactivationmails' => 262144,
+ 'pmboxwarning' => 524288,
+ 'showusercss' => 1048576,
+ 'receivefriendemailrequest' => 2097152,
+ 'vm_enable' => 8388608,
+ 'vm_contactonly' => 16777216,
+ 'pmdefaultsavecopy' => 33554432
+ ),
+ 'announcementoptions' => array(
+ 'allowbbcode' => 1,
+ 'allowhtml' => 2,
+ 'allowsmilies' => 4,
+ 'parseurl' => 8,
+ 'signature' => 16,
+ )
+ );
+
+ /**
+ * @brief Fetched Settings Cache
+ */
+ protected $settingsCache = array();
+
+ /**
+ * Get Setting Value - useful for global settings that need to be translated to group or member settings
+ *
+ * @param $key The setting key
+ * @return mixed
+ */
+ protected function _setting( $key )
+ {
+ if ( isset( $this->settingsCache[$key] ) )
+ {
+ return $this->settingsCache[$key];
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'value, defaultvalue', 'setting', array( "varname=?", $key ) )->first();
+
+ if ( $setting['value'] )
+ {
+ $this->settingsCache[$key] = $setting['value'];
+ }
+ else
+ {
+ $this->settingsCache[$key] = $setting['defaultvalue'];
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If we failed to find it, we probably will fail again on later attempts */
+ $this->settingsCache[$key] = NULL;
+ }
+
+ return $this->settingsCache[$key];
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ /* Attachment URLs are the same across VB 3.8 and VB 4 */
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'attachment.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->attachmentid, array( 'attachments', 'core_attachments' ) );
+
+ return \IPS\Http\Url::external( \IPS\Settings::i()->base_url . 'applications/core/interface/file/attachment.php' )->setQueryString( 'id', $data );
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ /* Tag URLs are straightforward */
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'tags.php' ) !== FALSE AND isset( \IPS\Request::i()->tag ) )
+ {
+ return \IPS\Http\Url::internal( "app=core&module=search&controller=search&tags=" . \IPS\Request::i()->tag, 'front', 'search' );
+ }
+ else
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ /* Profile URLs can be in one of 3 formats really...
+ * /member.php/1-name
+ * /members/1-name
+ * /member.php?u=1
+ */
+ $path = $url->data[ \IPS\Http\Url::COMPONENT_PATH ];
+ if( mb_strpos( $path, 'member.php' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->u ) )
+ {
+ $oldId = \IPS\Request::i()->u;
+ }
+ else
+ {
+ $queryStringPieces = explode( '-', mb_substr( $path, mb_strpos( $path, 'member.php/' ) + mb_strlen( 'member.php/' ) ) );
+ $oldId = $queryStringPieces[0];
+ }
+ }
+ elseif( preg_match( '#/members/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $oldId = (int) $matches[1];
+ }
+
+ if( isset( $oldId ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Vbulletin5.php b/applications/convert/sources/Software/Core/Vbulletin5.php
new file mode 100644
index 00000000..ad13d61f
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Vbulletin5.php
@@ -0,0 +1,1582 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin5 extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin (5.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin5";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertEmoticons' => array(
+ 'table' => 'smilie',
+ 'where' => NULL
+ ),
+ 'convertProfileFieldGroups' => array(
+ 'table' => 'profilefieldcategory',
+ 'where' => NULL
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'profilefield',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'usergroup',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'user',
+ 'where' => NULL
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'userlist',
+ 'where' => array( "type=?", 'ignore' )
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'pm',
+ 'where' => array( "parentpmid=?", 0 ),
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'privatemessage',
+ 'where' => array( "msgtype=?", 'message' ),
+ ),
+ 'convertRanks' => array(
+ 'table' => 'usertitle',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'pm':
+ try
+ {
+ $node = $this->db->select( 'nodeid', 'channel', array( "guid=?", 'vbulletin-4ecbdf567f3da8.31769341' ) )->first();
+
+ return $this->db->select( 'COUNT(*)', 'privatemessage', array( "privatemessage.msgtype=? AND node.parentid=?", 'message', $node ) )->join( 'node', 'privatemessage.nodeid = node.nodeid' )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return 0;
+ }
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Can we convert settings?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Settings Map
+ *
+ * @return array
+ */
+ public function settingsMap()
+ {
+ return array(
+ 'bbtitle' => 'board_name',
+ );
+ }
+
+ /**
+ * Settings Map Listing
+ *
+ * @return array
+ */
+ public function settingsMapList()
+ {
+ $settings = array();
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ try
+ {
+ $setting = $this->db->select( 'varname, value', 'setting', array( "varname=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ try
+ {
+ $title = $this->db->select( 'text', 'phrase', array( "varname=?", "setting_{$setting['varname']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $title = $setting['varname'];
+ }
+
+ $settings[$setting['varname']] = array( 'title' => $title, 'value' => $setting['value'], 'our_key' => $ours, 'our_title' => \IPS\Member::loggedIn()->language()->addToStack( $ours ) );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_vb5_preconvert';
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertEmoticons',
+ 'convertProfileFieldGroups',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers',
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertEmoticons':
+ $return['convertEmoticons'] = array(
+ 'emoticon_path' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_vb5_smilie_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ 'keep_existing_emoticons' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ )
+ );
+ break;
+
+ case 'convertProfileFieldGroups':
+ $return['convertProfileFieldGroups'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_groups' ), 'IPS\core\ProfileFields\Group' ) AS $group )
+ {
+ $options[$group->_id] = $group->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profilefieldcategory' ) AS $group )
+ {
+ $id = $group['profilefieldcategoryid']; # vB doesn't use spaces in column names and its driving me crazy typing it out
+
+ try
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}"] = $this->db->select( 'text', 'phrase', array( "varname=?", "category{$id}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}"] = "vBulletin Profile Group {$id}";
+ }
+
+ \IPS\Member::loggedIn()->language()->words["map_pfgroup_{$id}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfgroup_desc' );
+
+ $return['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'profilefield' ) AS $field )
+ {
+ try
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}"] = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}"] = "vBulletin Profile Field {$field['profilefieldid']}";
+ }
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['profilefieldid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'usergroup' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['usergroupid']}"] = $group['title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['usergroupid']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['usergroupid']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* We can only retain one type of photo */
+ $return['convertMembers']['photo_type'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'avatars',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'avatars' => \IPS\Member::loggedIn()->language()->addToStack( 'avatars' ), 'profile_photos' => \IPS\Member::loggedIn()->language()->addToStack( 'profile_photos' ) ) ),
+ 'field_hint' => NULL,
+ );
+
+ /* Find out where the photos live */
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( $value != 'database' AND !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ /* And decide what to do about these... */
+ foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'msn', 'skype', 'google' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Non-Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ //return nl2br( $post ); - I commented this out because it actually causes problems with LegacyParser, and most of what is used by vBulletin is actually handled by LegacyParser just fine, including linebreaks.
+
+ /* Fix Quotes */
+ $post = preg_replace( "#\[quote=([^\];]+?)\]#i", "[quote name='$1']", $post );
+ $post = preg_replace( "#\[quote=([^\];]+?);\d+\]#i", "[quote name='$1']", $post );
+ $post = preg_replace( "#\[quote=([^\];]+?);n\d+\]#i", "[quote name='$1']", $post );
+
+ /* Remove video tags and allow our parser to handle the embeds it supports */
+ $post = preg_replace( "#\[video=[a-z]+;[a-z0-9_]+\](.*?)\[\/video\]#i", '$1', $post );
+
+ return $post;
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'smilieid' );
+
+ foreach( $this->fetch( 'smilie', 'smilieid' ) AS $emoticon )
+ {
+ $filename = explode( '/', $emoticon['smiliepath'] );
+ $filename = array_pop( $filename );
+
+ $info = array(
+ 'id' => $emoticon['smilieid'],
+ 'typed' => $emoticon['smilietext'],
+ 'filename' => $filename,
+ 'emo_position' => $emoticon['displayorder'],
+ );
+
+ try
+ {
+ $imageCategory = $this->db->select( '*', 'imagecategory', array( "imagecategoryid=?", $emoticon['imagecategoryid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $imageCategory = array(
+ 'title' => "Converted",
+ 'displayorder' => 1,
+ );
+ }
+
+ $set = array(
+ 'set' => md5( $imageCategory['title'] ),
+ 'title' => $imageCategory['title'],
+ 'position' => $imageCategory['displayorder']
+ );
+
+ $libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $this->app->_session['more_info']['convertEmoticons']['emoticon_path'] );
+
+ $libraryClass->setLastKeyValue( $emoticon['smilieid'] );
+ }
+ }
+
+ /**
+ * Convert profile field groups
+ *
+ * @return void
+ */
+ public function convertProfileFieldGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profilefieldcategoryid' );
+
+ foreach( $this->fetch( 'profilefieldcategory', 'profilefieldcategoryid' ) AS $group )
+ {
+ try
+ {
+ $name = $this->db->select( 'text', 'phrase', array( "varname=?", "category{$group['profilefieldcategoryid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $name = "vBulletin Profile Group {$group['profilefieldcategoryid']}";
+ }
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFieldGroups']["map_pfgroup_{$group['profilefieldcategoryid']}"] : NULL;
+
+ $libraryClass->convertProfileFieldGroup( array(
+ 'pf_group_id' => $group['profilefieldcategoryid'],
+ 'pf_group_name' => $name,
+ 'pf_group_order' => $group['displayorder']
+ ), $merge );
+
+ $libraryClass->setLastKeyValue( $group['profilefieldcategoryid'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profilefieldid' );
+
+ foreach( $this->fetch( 'profilefield', 'profilefieldid' ) AS $field )
+ {
+ try
+ {
+ $name = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_title" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $name = "vBulletin Profile Field {$field['profilefieldid']}";
+ }
+
+ try
+ {
+ $desc = $this->db->select( 'text', 'phrase', array( "varname=?", "field{$field['profilefieldid']}_desc" ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $desc = "";
+ }
+
+ $merge = ( $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] != 'none' ) ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$field['profilefieldid']}"] : NULL;
+
+ if ( in_array( mb_strtolower( $field['type'] ), array( 'select', 'radio', 'checkbox', 'select_multiple' ) ) )
+ {
+ $content = json_encode( @\unserialize( $field['data'] ) );
+ }
+ else
+ {
+ $content = json_encode( array() );
+ }
+
+ $type = static::_pfieldMap( $field['type'] );
+
+ $multiple = 0;
+ if ( $field['type'] == 'select_multiple' )
+ {
+ $multiple = 1;
+ }
+
+ $info = array(
+ 'pf_id' => $field['profilefieldid'],
+ 'pf_name' => $name,
+ 'pf_desc' => $desc,
+ 'pf_type' => $type,
+ 'pf_content' => $content,
+ 'pf_not_null' => ( in_array( $field['required'], array( 1, 3 ) ) ) ? 1 : 0,
+ 'pf_member_hide' => $field['hidden'],
+ 'pf_max_input' => $field['maxlength'],
+ 'pf_member_edit' => ( $field['editable'] >= 1 ) ? 1 : 0,
+ 'pf_position' => $field['displayorder'],
+ 'pf_show_on_reg' => ( $field['required'] == 2 ) ? 1 : 0,
+ 'pf_input_format' => $field['regex'],
+ 'pf_admin_only' => ( $field['editable'] == 0 ) ? 1 : 0,
+ 'pf_group_id' => $field['profilefieldcategoryid'],
+ 'pf_multiple' => $multiple
+ );
+
+ $libraryClass->convertProfileField( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $field['profilefieldid'] );
+ }
+ }
+
+ /**
+ * Maps vBulletin Profile Field type to the IPS Equivalent
+ *
+ * @param string The vB Field Type
+ * @return string The IPS Field Type
+ */
+ protected static function _pfieldMap( $type )
+ {
+ switch( mb_strtolower( $type ) )
+ {
+ case 'select':
+ case 'radio':
+ case 'checkbox':
+ return ucwords( $type );
+ break;
+
+ case 'textarea':
+ return 'TextArea';
+ break;
+
+ case 'select_multiple':
+ return 'Select';
+ break;
+
+ /* Just do Text as default */
+ default:
+ return 'Text';
+ break;
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'usergroupid' );
+
+ foreach( $this->fetch( 'usergroup', 'usergroupid' ) AS $group )
+ {
+ /* <3 Closures */
+ $self = $this;
+ $checkpermission = function ( $name, $perm ) use ( $group, $self )
+ {
+ if ( $group[$name] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ };
+
+ /* Work out promotion */
+ $g_promotion = '-1&-1';
+ $gbw_promote_unit = 0;
+ try
+ {
+ $promotion = $this->db->select( '*', 'userpromotion', array( "usergroupid=?", $group['usergroupid'] ) )->first();
+
+ /* We only support Posts or Join Date */
+ if ( in_array( $promotion['strategy'], array( 17, 18 ) ) )
+ {
+ switch( $promotion['strategy'] )
+ {
+ case 17: # posts
+ $g_promotion = array( $promotion['joinusergroupid'], $promotion['posts'] );
+ break;
+
+ case 18: #joindate
+ $g_promotion = array( $promotion['joinusergroupid'], $promotion['date'] );
+ $gbw_promote_unit = 1;
+ break;
+ }
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Work out photo vars - vBulletin has a concept of avatars and profile photos, so we are just going to use the larger of the two */
+ $g_max_photo_vars = array();
+ $g_max_photo_vars[] = ( $group['profilepicmaxsize'] > $group['avatarmaxsize'] ) ? $group['profilepicmaxsize'] : $group['avatarmaxsize'];
+
+ /* We don't have individual controls for height and width, so work out which value is the largest and use that */
+ $highestValue = 0;
+ foreach( array( 'profilepicmaxheight', 'avatarmaxheight', 'profilepicmaxwidth', 'avatarmaxwidth' ) AS $value )
+ {
+ if ( $group[$value] > $highestValue )
+ {
+ $highestValue = $group[$value];
+ }
+ }
+
+ $g_max_photo_vars[] = $highestValue;
+ $g_max_photo_vars[] = $highestValue;
+
+ /* Work out signature limits */
+ $g_signature_limits = '1:::::';
+ if ( $checkpermission( 'genericpermissions', 'canusesignature' ) )
+ {
+ $g_signature_limits = array();
+ $g_signature_limits[] = 0;
+ $g_signature_limits[] = $group['sigmaximages'];
+ $g_signature_limits[] = $group['sigpicmaxwidth'];
+ $g_signature_limits[] = $group['sigpicmaxheight'];
+ $g_signature_limits[] = '';
+ $g_signature_limits[] = ( $group['sigmaxlines'] > 0 ) ? $group['sigmaxlines'] : '';
+ }
+
+ /* Let's disect all of these bit options */
+ $info = array(
+ 'g_id' => $group['usergroupid'],
+ 'g_name' => $group['title'],
+ 'g_view_board' => $checkpermission( 'forumpermissions', 'canview' ) ? 1 : 0,
+ 'g_mem_info' => $checkpermission( 'genericpermissions', 'canviewmembers' ) ? 1 : 0,
+ 'g_use_search' => $checkpermission( 'forumpermissions', 'cansearch' ) ? 1 : 0,
+ 'g_edit_profile' => $checkpermission( 'genericpermissions', 'canmodifyprofile' ) ? 1 : 0,
+ 'g_edit_posts' => $checkpermission( 'forumpermissions', 'caneditpost' ) ? 1 : 0,
+ 'g_delete_own_posts' => $checkpermission( 'forumpermissions', 'candeletepost' ) ? 1 : 0,
+ 'g_use_pm' => ( $group['pmquota'] > 0 ) ? 1 : 0,
+ 'g_is_supmod' => $checkpermission( 'adminpermissions', 'ismoderator' ) ? 1 : 0,
+ 'g_access_cp' => $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ? 1 : 0,
+ 'g_append_edit' => $checkpermission( 'genericoptions', 'showeditedby' ) ? 1 : 0, # Fun fact, I couldn't find this as it was actually in the place it should be rather than forumpermissions
+ 'g_access_offline' => $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ? 1 : 0,
+ 'g_attach_max' => ( $group['attachlimit'] > 0 ) ? $group['attachlimit'] : -1,
+ 'prefix' => $group['opentag'],
+ 'suffix' => $group['closetag'],
+ 'g_max_messages' => $group['pmquota'],
+ 'g_max_mass_pm' => $group['pmsendmax'],
+ 'g_promotion' => $g_promotion,
+ 'g_photo_max_vars' => $g_max_photo_vars,
+ 'g_bypass_badwords' => ( ( $checkpermission( 'adminpermissions', 'ismoderator' ) OR $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ) AND $this->_setting( 'ctCensorMod' ) ),
+ 'g_mod_preview' => !$checkpermission( 'forumpermissions', 'followforummoderation' ) ? 1 : 0,
+ 'g_signature_limits' => $g_signature_limits,
+ 'g_bitoptions' => array(
+ 'gbw_promote_unit_type' => $gbw_promote_unit, // Type of group promotion to use. 1 is days since joining, 0 is content count. Corresponds to g_promotion
+ 'gbw_no_status_update' => !$checkpermission( 'visitormessagepermissions', 'canmessageownprofile' ), // Can NOT post status updates
+ 'gbw_soft_delete_own' => $checkpermission( 'forumpermissions', 'candeletepost' ), // Allow users of this group to hide their own submitted content
+ 'gbw_allow_upload_bgimage' => 1, // Can upload a cover photo?
+ 'gbw_view_reps' => $checkpermission( 'genericpermissions2', 'canprofilerep' ), // Can view who gave reputation?
+ 'gbw_no_status_import' => !$checkpermission( 'visitormessagepermissions', 'canmessageownprofile' ), // Can NOT import status updates from Facebook/Twitter
+ 'gbw_disable_tagging' => !$checkpermission( 'genericpermissions', 'cancreatetag' ), // Can NOT use tags
+ 'gbw_disable_prefixes' => !$checkpermission( 'genericpermissions', 'cancreatetag' ), // Can NOT use prefixes
+ 'gbw_pm_override_inbox_full'=> $checkpermission( 'pmpermissions', 'canignorequota' ), // 1 means this group can send other members PMs even when that member's inbox is full
+ 'gbw_cannot_be_ignored' => ( ( $checkpermission( 'adminpermissions', 'ismoderator' ) OR $checkpermission( 'adminpermissions', 'cancontrolpanel' ) ) AND $this->_setting( 'ignoremods' ) ), // 1 means they cannot be ignored. 0 means they can
+ ),
+ 'g_pm_flood_mins' => $this->_setting( 'pmfloodtime' ) / 60,
+ 'g_post_polls' => $checkpermission( 'forumpermissions', 'canpostpoll' ) ? 1 : 0,
+ 'g_vote_polls' => $checkpermission( 'forumpermissions', 'canvote' ) ? 1 : 0,
+ 'g_topic_rate_setting' => $checkpermission( 'forumpermissions', 'canthreadrate' ) ? 1 : 0
+ );
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$group['usergroupid']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$group['usergroupid']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $group['usergroupid'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'user.userid' );
+
+ foreach( $this->fetch( 'user', 'user.userid', NULL, 'user.*, usertextfield.signature' )->join( 'usertextfield', 'user.userid = usertextfield.userid' ) AS $user )
+ {
+ /* <3 Closures */
+ $self = $this;
+ $checkpermission = function ( $name, $perm ) use ( $user, $self )
+ {
+ if ( $name == 'useroptions' )
+ {
+ $key = 'options';
+ }
+ else
+ {
+ $key = $name;
+ }
+
+ if ( $user[$key] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+ else
+ {
+ return FALSE;
+ }
+ };
+
+ /* Fetch our last warning */
+ $warn_lastwarn = 0;
+ try
+ {
+ $warn_lastwarn = $this->db->select( 'actiondateline', 'infraction', array( "infracteduserid=?", $user['userid'] ), "actiondateline DESC", 1 )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Birthday */
+ if ( $user['birthday'] )
+ {
+ list( $bday_month, $bday_day, $bday_year ) = explode( '-', $user['birthday'] );
+
+ if ( $bday_year == '0000' )
+ {
+ $bday_year = NULL;
+ }
+ }
+ else
+ {
+ $bday_month = $bday_day = $bday_year = NULL;
+ }
+
+ /* Auto Track */
+ $auto_track = 0;
+ switch( $user['autosubscribe'] )
+ {
+ case 1:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'immediate' );
+ break;
+
+ case 2:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'daily' );
+ break;
+
+ case 3:
+ $auto_track = array( 'content' => true, 'comments' => true, 'method' => 'weekly' );
+ break;
+ }
+
+ /* User Banned */
+ $temp_ban = 0;
+ try
+ {
+ $temp_ban = $this->db->select( 'liftdate', 'userban', array( "userid=?", $user['userid'] ) )->first();
+
+ if ( $temp_ban == 0 )
+ {
+ $temp_ban = -1;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Figure Out Password - they differ between 5.0 and 5.1 */
+ if ( !isset( $user['password'] ) AND ( isset( $user['token'] ) AND isset( $user['scheme'] ) ) )
+ {
+ $pass = $user['token'];
+ $extra = $user['scheme'];
+ }
+ else
+ {
+ $pass = $user['password'];
+ $extra = $user['salt'];
+ }
+
+ /* Main Members Table */
+ $info = array(
+ 'member_id' => $user['userid'],
+ 'member_group_id' => $user['usergroupid'],
+ 'mgroup_others' => $user['membergroupids'],
+ 'name' => $user['username'],
+ 'email' => $user['email'],
+ 'member_title' => $user['usertitle'],
+ 'joined' => $user['joindate'],
+ 'ip_address' => $user['ipaddress'],
+ 'warn_level' => $user['ipoints'],
+ 'warn_lastwarn' => $warn_lastwarn,
+ 'bday_day' => $bday_day,
+ 'bday_month' => $bday_month,
+ 'bday_year' => $bday_year,
+ 'last_visit' => $user['lastvisit'],
+ 'last_activity' => $user['lastactivity'],
+ 'auto_track' => $auto_track,
+ 'temp_ban' => $temp_ban,
+ 'members_profile_views' => $user['profilevisits'],
+ 'conv_password' => $pass,
+ 'conv_password_extra' => $extra,
+ 'members_bitoptions' => array(
+ 'view_sigs' => $checkpermission( 'useroptions', 'showsignatures' ), // View signatures?
+ 'coppa_user' => $checkpermission( 'useroptions', 'coppauser' ), // Was the member validated using coppa?
+ 'timezone_override' => !$checkpermission( 'useroptions', 'dstauto' ), // If TRUE, user's timezone will not be detected automatically
+ ),
+ 'members_bitoptions2' => array(
+ 'show_pm_popup' => $user['pmpopup'], // "Show pop-up when I have a new message"
+ ),
+ 'pp_setting_count_comments' => $checkpermission( 'useroptions', 'vm_enable' ),
+ 'pp_reputation_points' => $user['reputation'],
+ 'signature' => $user['signature'] ?: '',
+ 'allow_admin_mails' => $checkpermission( 'useroptions', 'adminemail' ),
+ 'members_disable_pm' => !$checkpermission( 'useroptions', 'receivepm' ),
+ 'member_posts' => $user['posts'],
+ );
+
+ /* Profile Fields */
+ try
+ {
+ $profileFields = $this->db->select( '*', 'userfield', array( "userid=?", $user['userid'] ) )->first();
+
+ unset( $profileFields['userid'] );
+ unset( $profileFields['temp'] );
+
+ /* Basic fields - we only need ID => Value, the library will handle the rest */
+ foreach( $profileFields AS $key => $value )
+ {
+ $profileFields[ str_replace( 'field', '', $key ) ] = $value;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $profileFields = array();
+ }
+
+ /* Pseudo Fields */
+ foreach( array( 'homepage', 'icq', 'aim', 'yahoo', 'msn', 'skype', 'google' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $profileFields[$pseudo] = $user[$pseudo];
+ }
+
+ /* Profile Photos */
+ $firstTable = 'customavatar';
+ $secondTable = 'customprofilepic';
+ if ( $this->app->_session['more_info']['convertMembers']['photo_type'] == 'profile_photos' )
+ {
+ $firstTable = 'customprofilepic';
+ $secondTable = 'customavatar';
+ }
+ if ( $this->app->_session['more_info']['convertMembers']['photo_location'] == 'database' )
+ {
+ try
+ {
+ foreach( $this->db->select( 'filedata, filename', $firstTable, array( "userid=?", $user['userid'] ) )->first() AS $key => $value )
+ {
+ if ( $key == 'filedata' )
+ {
+ $filedata = $value;
+ }
+ else
+ {
+ $filename = $value;
+ }
+ }
+ $filepath = NULL;
+ }
+ catch( \UnderflowException $e )
+ {
+ try
+ {
+ foreach( $this->db->select( 'filedata, filename', $secondTable, array( "userid=?", $user['userid'] ) )->first() AS $key => $value )
+ {
+ if ( $key == 'filedata' )
+ {
+ $filedata = $value;
+ }
+ else
+ {
+ $filename = $value;
+ }
+ }
+ $filepath = NULL;
+ }
+ catch( \UnderflowException $e )
+ {
+ list( $filedata, $filename, $filepath ) = array( NULL, NULL, NULL );
+ }
+ }
+ }
+ else
+ {
+ $filepath = $this->app->_session['more_info']['convertMembers']['photo_location'];
+ $first = 'avatar';
+ $second = 'profilepic';
+
+ if ( $this->app->_session['more_info']['convertMembers']['photo_type'] == 'profile_photos' )
+ {
+ $first = 'profilepic';
+ $second = 'avatar';
+ }
+
+ try
+ {
+ try
+ {
+ $ext = $this->db->select( 'filename', $firstTable, array( "userid=?", $user['userid'] ) )->first();
+ $ext = explode( '.', $ext );
+ $ext = array_pop( $ext );
+
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.'. $ext ) )
+ {
+ $filename = $first . $user['userid'] . '_' . $user[$first . 'revision'] . '.'. $ext;
+ }
+ else
+ {
+ /* Throw an exception so we can try the other */
+ throw new \UnderflowException;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $ext = $this->db->select( 'filename', $secondTable, array( "userid=?", $user['userid'] ) )->first();
+ $ext = explode( '.', $ext );
+ $ext = array_pop( $ext );
+
+ if ( file_exists( rtrim( $filepath, '/' ) . '/' . $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.'. $ext ) )
+ {
+ $filename = $second . $user['userid'] . '_' . $user[$second . 'revision'] . '.'. $ext;
+ }
+ else
+ {
+ /* Throw an exception so we can try the other */
+ throw new \UnderflowException;
+ }
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ list( $filedata, $filename, $filepath ) = array( NULL, NULL, NULL );
+ }
+ }
+
+ $libraryClass->convertMember( $info, $profileFields, $filename, $filepath, $filedata );
+
+ /* Any friends need converting to followers? */
+ foreach( $this->db->select( '*', 'userlist', array( "type=? AND friend=? AND userid=?", 'buddy', 'yes', $user['userid'] ) ) AS $follower )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'members',
+ 'follow_rel_id' => $follower['relationid'],
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $follower['userid'],
+ ) );
+ }
+
+ /* And warn logs made on the profile - we'll do content specific later */
+ foreach( $this->db->select( '*', 'infraction', array( "infracteduserid=? AND infractednodeid=?", $user['userid'], 0 ) ) AS $warn )
+ {
+ try
+ {
+ $node = $this->db->select( 'userid, description', 'node', array( "nodeid=?", $warn['nodeid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['nodeid'],
+ 'wl_member' => $warn['infracteduserid'],
+ 'wl_moderator' => $node['userid'],
+ 'wl_date' => $warn['actiondateline'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $node['description'],
+ 'wl_note_mods' => $warn['customreason'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $user['userid'] );
+ }
+ }
+
+ /**
+ * Convert one or more settings
+ *
+ * @param array $settings Settings to convert
+ * @return void
+ */
+ public function convertSettings( $settings=array() )
+ {
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ if ( !isset( $values[$ours] ) OR $values[$ours] == FALSE )
+ {
+ continue;
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'value', 'setting', array( "varname=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $setting ), array( "conf_key=?", $ours ) );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'userlist', 'userid', array( "type=?", 'ignore' ) ) AS $ignore )
+ {
+ $info = array(
+ 'ignore_id' => $ignore['userid'] . '-' . $ignore['relationid'],
+ 'ignore_owner_id' => $ignore['userid'],
+ 'ignore_ignore_id' => $ignore['relationid'],
+ );
+
+ foreach( \IPS\core\Ignore::types() AS $type )
+ {
+ $info['ignore_' . $type] = 1;
+ }
+
+ $libraryClass->convertIgnoredUser( $info );
+ }
+ }
+
+ /**
+ * Convert PMs
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $node = $this->db->select( 'nodeid', 'channel', array( "guid=?", 'vbulletin-4ecbdf567f3da8.31769341' ) )->first();
+
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'privatemessage.nodeid' );
+
+ foreach( $this->fetch( 'privatemessage', 'privatemessage.nodeid', array( "privatemessage.msgtype=? AND node.parentid=?", 'message', $node ) )->join( 'node', 'privatemessage.nodeid = node.nodeid' )->join( 'text', 'text.nodeid = node.nodeid' ) AS $pm )
+ {
+ try
+ {
+ $replies = $this->db->select( 'COUNT(*)', 'node', array( "parentid=?", $pm['nodeid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $replies = 1;
+ }
+
+ $to = array( $pm['userid'] => $pm['userid'] );
+ $toId = NULL;
+ foreach( $this->db->select( 'userid', 'node', array( "parentid=?", $pm['nodeid'] ) ) AS $userid )
+ {
+ $to[$userid] = $userid;
+ if ( is_null( $toId ) AND $userid != $pm['userid'] )
+ {
+ $toId = $userid;
+ }
+ }
+
+ $topic = array(
+ 'mt_id' => $pm['nodeid'],
+ 'mt_date' => $pm['publishdate'],
+ 'mt_title' => $pm['title'],
+ 'mt_starter_id' => $pm['userid'],
+ 'mt_start_time' => $pm['publishdate'],
+ 'mt_last_post_time' => $pm['lastcontent'],
+ 'mt_to_count' => count( $to ),
+ 'mt_to_member_id' => $toId,
+ 'mt_replies' => $replies,
+ 'mt_first_msg_id' => $pm['nodeid'],
+ );
+
+ $maps = array();
+
+ /* First Map */
+ $maps[ $pm['userid'] ] = array(
+ 'map_user_id' => $pm['userid'],
+ 'map_topic_id' => $pm['nodeid'],
+ 'map_user_active' => 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => 0,
+ 'map_is_starter' => ( $pm['userid'] == $pm['userid'] ) ? 1 : 0,
+ );
+
+ foreach( $this->db->select( '*', 'node', array( "parentid=?", $pm['nodeid'] ) ) AS $post )
+ {
+ $maps[ $post['userid'] ] = array(
+ 'map_user_id' => $post['userid'],
+ 'map_topic_id' => $pm['nodeid'],
+ 'map_user_active' => 1,
+ 'map_user_banned' => 0,
+ 'map_has_unread' => 0,
+ 'map_is_starter' => ( $post['userid'] == $pm['userid'] ) ? 1 : 0,
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $pm['nodeid'] );
+ }
+ }
+
+ /**
+ * Convert PM replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $node = $this->db->select( 'nodeid', 'channel', array( "guid=?", 'vbulletin-4ecbdf567f3da8.31769341' ) )->first();
+
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'privatemessage.nodeid' );
+
+ foreach( $this->fetch( 'privatemessage', 'privatemessage.nodeid', array( "privatemessage.msgtype=?", 'message' ), 'privatemessage.*, node.parentid, node.publishdate, node.oldid, node.userid, node.ipaddress, node.description, text.rawtext' )->join( 'node', 'privatemessage.nodeid = node.nodeid' )->join( 'text', 'text.nodeid = node.nodeid' ) AS $pm )
+ {
+ if ( $pm['oldid'] AND $this->db->checkForTable( 'pmtext' ) )
+ {
+ try
+ {
+ $pm['rawtext'] = $this->db->select( 'message', 'pmtext', array( "pmtextid=?", $pm['oldid'] ) )->first();
+ }
+ catch( \UnderflowException $e ) { }
+ }
+
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $pm['nodeid'],
+ 'msg_topic_id' => ( $pm['parentid'] == $node ) ? $pm['nodeid'] : $pm['parentid'],
+ 'msg_date' => $pm['publishdate'],
+ 'msg_post' => ( isset( $pm['rawtext'] ) ) ? $pm['rawtext'] : $pm['description'],
+ 'msg_author_id' => $pm['userid'],
+ 'msg_ip_address' => $pm['ipaddress'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $pm['nodeid'] );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'usertitleid' );
+
+ foreach( $this->fetch( 'usertitle', 'usertitleid' ) AS $rank )
+ {
+ $libraryClass->convertRank( array(
+ 'id' => $rank['usertitleid'],
+ 'posts' => $rank['minposts'],
+ 'title' => $rank['title']
+ ) );
+
+ $libraryClass->setLastKeyValue( $rank['usertitleid'] );
+ }
+ }
+
+ /* !vBulletin Specific Stuff */
+ /**
+ * @brief Silly Bit Options for Groups. Typically we would leave out app specific options (such as Forums here) however we need them for some general permissions, like uploading.
+ * @note This is public simply because I do not want to do this ever again if it ever changes.
+ */
+ public static $bitOptions = array(
+ 'forumpermissions' => array(
+ 'canview' => 1,
+ 'canviewothers' => 2,
+ 'cansearch' => 4,
+ 'canemail' => 8,
+ 'canpostnew' => 16,
+ 'canreplyown' => 32,
+ 'canreplyothers' => 64,
+ 'caneditpost' => 128,
+ 'candeletepost' => 256,
+ 'candeletethread' => 512,
+ 'canopenclose' => 1024,
+ 'canmove' => 2048,
+ 'cangetattachment' => 4096,
+ 'canpostattachment' => 8192,
+ 'canpostpoll' => 16384,
+ 'canvote' => 32768,
+ 'canthreadrate' => 65536,
+ 'followforummoderation' => 131072,
+ 'canseedelnotice' => 262144,
+ 'canviewthreads' => 524288,
+ 'cantagown' => 1048576,
+ 'cantagothers' => 2097152,
+ 'candeletetagown' => 4194304,
+ 'canseethumbnails' => 8388608,
+ 'canattachmentcss' => 16777216,
+ 'bypassdoublepost' => 33554432,
+ 'canwrtmembers' => 67108864,
+ ),
+ 'pmpermissions' => array(
+ 'cantrackpm' => 1,
+ 'candenypmreceipts' => 2,
+ 'canignorequota' => 4,
+ ),
+ 'wolpermissions' => array(
+ 'canwhosonline' => 1,
+ 'canwhosonlineip' => 2,
+ 'canwhosonlinefull' => 4,
+ 'canwhosonlinebad' => 8,
+ 'canwhosonlinelocation' => 16,
+ ),
+ 'adminpermissions' => array(
+ 'ismoderator' => 1,
+ 'cancontrolpanel' => 2,
+ 'canadminsettings' => 4,
+ 'canadminstyles' => 8,
+ 'canadminlanguages' => 16,
+ 'canadminforums' => 32,
+ 'canadminthreads' => 64,
+ 'canadmincalendars' => 128,
+ 'canadminusers' => 256,
+ 'canadminpermissions' => 512,
+ 'canadminfaq' => 1024,
+ 'canadminimages' => 2048,
+ 'canadminbbcodes' => 4096,
+ 'canadmincron' => 8192,
+ 'canadminmaintain' => 16384,
+ 'canadminplugins' => 65536,
+ 'canadminnotices' => 131072,
+ 'canadminmodlog' => 262144,
+ 'cansitemap' => 524288,
+ 'canadminads' => 1048576,
+ 'canadmintags' => 2097152,
+ 'canadminblocks' => 4194304,
+ 'cansetdefaultprofile' => 8388608,
+ ),
+ 'genericpermissions' => array(
+ 'canviewmembers' => 1,
+ 'canmodifyprofile' => 2,
+ 'caninvisible' => 4,
+ 'canviewothersusernotes' => 8,
+ 'canmanageownusernotes' => 16,
+ 'canseehidden' => 32,
+ 'canbeusernoted' => 64,
+ 'canprofilepic' => 128,
+ 'canseeownrep' => 256,
+ 'cananimateprofilepic' => 134217728,
+ 'canuseavatar' => 512,
+ 'canusesignature' => 1024,
+ 'canusecustomtitle' => 2048,
+ 'canseeprofilepic' => 4096,
+ 'canviewownusernotes' => 8192,
+ 'canmanageothersusernotes' => 16384,
+ 'canpostownusernotes' => 32768,
+ 'canpostothersusernotes' => 65536,
+ 'caneditownusernotes' => 131072,
+ 'canseehiddencustomfields' => 262144,
+ 'canuserep' => 524288,
+ 'canhiderep' => 1048576,
+ 'cannegativerep' => 2097152,
+ 'cangiveinfraction' => 4194304,
+ 'cananimateavatar' => 67108864,
+ 'canseeinfraction' => 8388608,
+ 'cangivearbinfraction' => 536870912,
+ 'canreverseinfraction' => 16777216,
+ 'cansearchft_bool' => 33554432,
+ 'canemailmember' => 268435456,
+ 'cancreatetag' => 1073741824,
+ ),
+ 'genericpermissions2' => array(
+ 'canusefriends' => 1,
+ 'canprofilerep' => 2,
+ 'canwgomembers' => 4,
+ ),
+ 'genericoptions' => array(
+ 'showgroup' => 1,
+ 'showbirthday' => 2,
+ 'showmemberlist' => 4,
+ 'showeditedby' => 8,
+ 'allowmembergroups' => 16,
+ 'isnotbannedgroup' => 32,
+ 'requirehvcheck' => 64,
+ ),
+ 'signaturepermissions' => array(
+ 'canbbcode' => 131072,
+ 'canbbcodebasic' => 1,
+ 'canbbcodecolor' => 2,
+ 'canbbcodesize' => 4,
+ 'canbbcodefont' => 8,
+ 'canbbcodealign' => 16,
+ 'canbbcodelist' => 32,
+ 'canbbcodelink' => 64,
+ 'canbbcodecode' => 128,
+ 'canbbcodephp' => 256,
+ 'canbbcodehtml' => 512,
+ 'canbbcodequote' => 1024,
+ 'allowimg' => 2048,
+ 'allowvideo' => 262144,
+ 'allowsmilies' => 4096,
+ 'allowhtml' => 8192,
+ 'cansigpic' => 32768,
+ 'cananimatesigpic' => 65536,
+ ),
+ 'visitormessagepermissions' => array(
+ 'canmessageownprofile' => 1,
+ 'canmessageothersprofile' => 2,
+ 'caneditownmessages' => 4,
+ 'candeleteownmessages' => 8,
+ 'canmanageownprofile' => 32,
+ 'followforummoderation' => 16,
+ ),
+ 'useroptions' => array(
+ 'showsignatures' => 1,
+ 'showavatars' => 2,
+ 'showimages' => 4,
+ 'coppauser' => 8,
+ 'adminemail' => 16,
+ 'showvcard' => 32,
+ 'dstauto' => 64,
+ 'dstonoff' => 128,
+ 'showemail' => 256,
+ 'invisible' => 512,
+ 'showreputation' => 1024,
+ 'receivepm' => 2048,
+ 'emailonpm' => 4096,
+ 'hasaccessmask' => 8192,
+ 'vbasset_enable' => 16384,
+ 'postorder' => 32768,
+ 'receivepmbuddies' => 131072,
+ 'noactivationmails' => 262144,
+ 'pmboxwarning' => 524288,
+ 'showusercss' => 1048576,
+ 'receivefriendemailrequest' => 2097152,
+ 'vm_enable' => 8388608,
+ 'vm_contactonly' => 16777216,
+ 'pmdefaultsavecopy' => 33554432
+ ),
+ 'announcementoptions' => array(
+ 'allowbbcode' => 1,
+ 'allowhtml' => 2,
+ 'allowsmilies' => 4,
+ 'parseurl' => 8,
+ 'signature' => 16,
+ )
+ );
+
+ /**
+ * @brief Fetched Settings Cache
+ */
+ protected $settingsCache = array();
+
+ /**
+ * Get Setting Value - useful for global settings that need to be translated to group or member settings
+ *
+ * @param $key The setting key
+ * @return mixed
+ */
+ protected function _setting( $key )
+ {
+ if ( isset( $this->settingsCache[$key] ) )
+ {
+ return $this->settingsCache[$key];
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'value, defaultvalue', 'setting', array( "varname=?", $key ) )->first();
+
+ if ( $setting['value'] )
+ {
+ $this->settingsCache[$key] = $setting['value'];
+ }
+ else
+ {
+ $this->settingsCache[$key] = $setting['defaultvalue'];
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If we failed to find it, we probably will fail again on later attempts */
+ $this->settingsCache[$key] = NULL;
+ }
+
+ return $this->settingsCache[$key];
+ }
+
+ /**
+ * Helper method to retrieve forums from nodes
+ *
+ * @return void
+ */
+ protected function fetchForums()
+ {
+ $forums = array();
+
+ foreach( $this->db->select( '*', 'node', array( "node.nodeid<>2 AND closure.parent=? AND node.contenttypeid=?", $this->fetchType( 'Thread' ), $this->fetchType( 'Channel' ) ) )->join( 'closure', "closure.child = node.nodeid" ) AS $node )
+ {
+ $forums[$node['nodeid']] = $node;
+ }
+
+ return $forums;
+ }
+
+ /**
+ * @brief Types Cache
+ */
+ protected $typesCache = array();
+
+ /**
+ * Helper method to retrieve content type ids
+ *
+ * @return void
+ */
+ protected function fetchType( $type )
+ {
+ if ( !empty( $this->typesCache ) )
+ {
+ return $this->typesCache[ $type ];
+ }
+ else
+ {
+ foreach( $this->db->select( '*', 'contenttype' ) AS $contenttype )
+ {
+ $this->typesCache[ $contenttype['class'] ] = $contenttype['contenttypeid'];
+ }
+
+ return $this->typesCache[ $type ];
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/member/([0-9]+)\-#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $matches[1], array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Wordpress.php b/applications/convert/sources/Software/Core/Wordpress.php
new file mode 100644
index 00000000..16082da7
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Wordpress.php
@@ -0,0 +1,234 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 12 December 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Wordpress extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "WordPress (4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "wordpress";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertMembers' => array(
+ 'table' => 'users',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_wordpress_preconvert';
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertMembers',
+ );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return nl2br( $post );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertMembers':
+ $return['convertMembers']['username'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'display_name',
+ 'field_required' => TRUE,
+ 'field_extra' => array( 'options' => array( 'username' => \IPS\Member::loggedIn()->language()->addToStack( 'user_name' ), 'display_name' => \IPS\Member::loggedIn()->language()->addToStack( 'display_name' ) ) ),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'username_hint' ),
+ );
+
+ /* Pseudo Fieds */
+ foreach( array( 'first_name', 'last_name', 'user_url' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => ucwords( str_replace( '_', ' ', $field ) ) ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'users.ID' );
+
+ foreach( $this->fetch( 'users', 'users.ID', NULL, 'users.*, usermeta.meta_value as user_level' )->join( 'usermeta', "users.ID=usermeta.user_id AND usermeta.meta_key='wp_user_level'" ) AS $user )
+ {
+ /* Main Members Table */
+ $info = array(
+ 'member_id' => $user['ID'],
+ 'ips_group_id' => $user['user_level'] > 9 ? \IPS\Settings::i()->admin_group : \IPS\Settings::i()->member_group,
+ 'name' => $this->app->_session['more_info']['convertMembers']['username'] == 'username' ? $user['user_login'] : $user['display_name'],
+ 'email' => $user['user_email'],
+ 'joined' => new \IPS\DateTime( $user['user_registered'] ),
+ 'conv_password' => $user['user_pass'],
+ 'pp_photo_type' => 'gravatar',
+ 'pp_gravatar' => $user['user_email'], # WordPress always uses gravatar, as it's their service
+ );
+
+ $fields = array();
+ foreach( array( 'first_name', 'last_name', 'user_url' ) AS $field )
+ {
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$field}"] != 'no_convert' )
+ {
+ try
+ {
+ $fieldId = $this->app->getLink( $field, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $field,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$field}"],
+ 'pf_desc' => '',
+ 'pf_type' => 'Text',
+ 'pf_content' => '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ if ( isset( $user[$field] ) )
+ {
+ $fields[$field] = $user[$field];
+ }
+ else
+ {
+ $fields[$field] = '';
+ }
+ }
+ }
+
+ $libraryClass->convertMember( $info, $fields );
+ $libraryClass->setLastKeyValue( $user['ID'] );
+ }
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches" );
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Core/Xenforo.php b/applications/convert/sources/Software/Core/Xenforo.php
new file mode 100644
index 00000000..25002ce0
--- /dev/null
+++ b/applications/convert/sources/Software/Core/Xenforo.php
@@ -0,0 +1,1324 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Core;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenforo extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ return "XenForo (1.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ return 'xenforo';
+ }
+
+ /**
+ * Returns a block of text, or a language string, that explains what the admin must do to start this conversion
+ *
+ * @return string
+ */
+ public static function getPreConversionInformation()
+ {
+ return 'convert_xf_preconvert';
+ }
+
+ /**
+ * Uses Prefix
+ *
+ * @return bool
+ */
+ public static function usesPrefix()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertEmoticons' => array(
+ 'table' => 'xf_smilie',
+ 'where' => NULL
+ ),
+ 'convertProfileFields' => array(
+ 'table' => 'xf_user_field',
+ 'where' => NULL
+ ),
+ 'convertGroups' => array(
+ 'table' => 'xf_user_group',
+ 'where' => NULL
+ ),
+ 'convertMembers' => array(
+ 'table' => 'xf_user',
+ 'where' => NULL
+ ),
+ 'convertWarnReason' => array(
+ 'table' => 'xf_warning_definition',
+ 'where' => NULL
+ ),
+ 'convertStatuses' => array(
+ 'table' => 'xf_profile_post',
+ 'where' => NULL
+ ),
+ 'convertStatusReplies' => array(
+ 'table' => 'xf_profile_post_comment',
+ 'where' => NULL
+ ),
+ 'convertIgnoredUsers' => array(
+ 'table' => 'xf_user_ignored',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessages' => array(
+ 'table' => 'xf_conversation_master',
+ 'where' => NULL
+ ),
+ 'convertPrivateMessageReplies' => array(
+ 'table' => 'xf_conversation_message',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'xf_attachment',
+ 'where' => array( "content_type=?", 'conversation_message' )
+ ),
+ 'convertRanks' => array(
+ 'table' => 'xf_user_title_ladder',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Can we convert settings?
+ *
+ * @return boolean
+ */
+ public static function canConvertSettings()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of Conversion Methods that require more information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments',
+ 'convertEmoticons',
+ 'convertProfileFields',
+ 'convertGroups',
+ 'convertMembers',
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch ( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+
+ case 'convertEmoticons':
+ /* XenForo stores emoticons either as a remotely linked image, or relative to the installation path, so we need to change the verbiage here a bit. */
+ \IPS\Member::loggedIn()->language()->words['emoticon_path'] = \IPS\Member::loggedIn()->language()->addToStack( 'source_path', FALSE, array( 'sprintf' => array( 'XenForo' ) ) );
+ $return['convertEmoticons'] = array(
+ 'emoticon_path' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ 'keep_existing_emoticons' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Checkbox',
+ 'field_default' => TRUE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ )
+ );
+ break;
+
+ case 'convertProfileFields':
+ $return['convertProfileFields'] = array();
+
+ $options = array();
+ $options['none'] = \IPS\Member::loggedIn()->language()->addToStack( 'none' );
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_pfields_data' ), 'IPS\core\ProfileFields\Field' ) AS $field )
+ {
+ $options[$field->_id] = $field->_title;
+ }
+
+ foreach( $this->db->select( '*', 'xf_user_field' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}"] = $this->getPhrase( "user_field_{$field['field_id']}" );
+ \IPS\Member::loggedIn()->language()->words["map_pfield_{$field['field_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_pfield_desc' );
+
+ $return['convertProfileFields']["map_pfield_{$field['field_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+
+ case 'convertGroups':
+ $return['convertGroups'] = array();
+
+ $options = array();
+ $options['none'] = 'None';
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_groups' ), 'IPS\Member\Group' ) AS $group )
+ {
+ $options[$group->g_id] = $group->name;
+ }
+
+ foreach( $this->db->select( '*', 'xf_user_group' ) AS $group )
+ {
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['user_group_id']}"] = $group['title'];
+ \IPS\Member::loggedIn()->language()->words["map_group_{$group['user_group_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'map_group_desc' );
+
+ $return['convertGroups']["map_group_{$group['user_group_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array( 'options' => $options ),
+ 'field_hint' => NULL,
+ );
+ }
+ break;
+ case 'convertMembers':
+ $return['convertMembers'] = array();
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['photo_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'photo_location_nodb_desc' );
+ $return['convertMembers']['photo_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_avatar_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+
+ foreach( array( 'homepage', 'location', 'occupation', 'about', 'gender' ) AS $field )
+ {
+ \IPS\Member::loggedIn()->language()->words["field_{$field}"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field', FALSE, array( 'sprintf' => $field ) );
+ \IPS\Member::loggedIn()->language()->words["field_{$field}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'pseudo_field_desc' );
+ $return['convertMembers']["field_{$field}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'no_convert',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'no_convert' => \IPS\Member::loggedIn()->language()->addToStack( 'no_convert' ),
+ 'create_field' => \IPS\Member::loggedIn()->language()->addToStack( 'create_field' ),
+ ),
+ 'userSuppliedInput' => 'create_field'
+ ),
+ 'field_hint' => NULL
+ );
+ }
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Settings Map
+ *
+ * @return array
+ */
+ public function settingsMap()
+ {
+ return array(
+ 'boardTitle' => 'board_name',
+ );
+ }
+
+ /**
+ * Settings Map Listing
+ *
+ * @return array
+ */
+ public function settingsMapList()
+ {
+ $settings = array();
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ try
+ {
+ $setting = $this->db->select( 'option_value', 'xf_option', array( "option_id=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ $title = $this->getPhrase( 'option_' . $setting );
+
+ if ( !$title )
+ {
+ $title = $setting;
+ }
+
+ $settings[ $theirs ] = array( 'title' => $title, 'value' => $setting, 'our_key' => $ours, 'our_title' => \IPS\Member::loggedIn()->language()->addToStack( $ours ) );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase
+ * @return string|null
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Finish
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Search Index Rebuild */
+ \IPS\Content\Search\Index::i()->rebuild();
+
+ /* Clear Cache and Store */
+ \IPS\Data\Store::i()->clearAll();
+ \IPS\Data\Cache::i()->clearAll();
+
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_message_posts', 'extension' => 'core_Messaging' ), 2, array( 'app', 'link', 'extension' ) );
+ \IPS\Task::queue( 'convert', 'RebuildNonContent', array( 'app' => $this->app->app_id, 'link' => 'core_members', 'extension' => 'core_Signatures' ), 2, array( 'app', 'link', 'extension' ) );
+
+ /* Content Counts */
+ \IPS\Task::queue( 'core', 'RecountMemberContent', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\core\Messenger\Message' ), 3, array( 'class' ) );
+
+ /* First Post Data */
+ \IPS\Task::queue( 'convert', 'RebuildConversationFirstIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+
+ return array( "f_search_index_rebuild", "f_clear_caches", "f_rebuild_pms", "f_signatures_rebuild" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ // run everything through htmlspecialchars to prevent XSS ( @see http://community.invisionpower.com/resources/bugs.html/_/ips-extras/converters/possible-xf-converter-xss-vector-r38108 )
+ $post = htmlspecialchars( $post, \IPS\HTMLENTITIES | ENT_QUOTES, 'UTF-8', FALSE );
+
+ // find YouTube ID's and replace.
+ $post = preg_replace( '#\[media=youtube\](.+?)\[/media\]#i', '[media]http://www.youtube.com/watch?v=$1[/media]', $post );
+
+ /* Mentions */
+ preg_match_all( '#\[user=(\d+)\](.+?)\[\/user\]#i', $post, $matches );
+
+ if ( count( $matches ) )
+ {
+ if ( isset( $matches[1] ) )
+ {
+ $mentions = array();
+ foreach( $matches[1] AS $k => $v )
+ {
+ if ( isset( $matches[2][$k] ) )
+ {
+ $name = trim( $matches[2][$k], '@' );
+ $mentions[$name] = $v;
+ }
+ }
+
+ $maps = array();
+ $urls = array();
+ $cardUrls = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'name', array_keys( $mentions ) ) ) ), 'IPS\Member' ) AS $member )
+ {
+ $maps[$member->name] = $member->member_id;
+ $urls[$member->name] = $member->url();
+ $cardUrls[$member->name] = $member->url()->setQueryString( 'do', 'hovercard' );
+ }
+
+ foreach( $mentions AS $member_name => $member_id )
+ {
+ $maps[$member_name] = preg_quote( $maps[$member_name], '#' );
+ $member_name = preg_quote( $member_name, '#' );
+
+ $post = preg_replace( "#\[user={$member_id}\]\@{$member_name}\[\/user\]#i", "@{$member_name} ", $post );
+ }
+ }
+ }
+
+ /* Quotes */
+ $post = preg_replace( '#\[quote="([a-zA-Z0-9]+)(,(.?)+)?"]#i', "[quote name='$1']", $post );
+
+ // finally, give us the post back.
+ return $post;
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'xf_attachment.attachment_id' );
+
+ $it = $this->fetch( 'xf_attachment', 'xf_attachment.attachment_id', array( "content_type=?", 'conversation_message' ) )->join( 'xf_attachment_data', 'xf_attachment.data_id = xf_attachment_data.data_id' );
+
+ foreach( $it AS $row )
+ {
+ $conversation = $this->db->select( 'conversation_id', 'xf_conversation_message', array( "message_id=?", $row['content_id'] ) )->first();
+
+ $map = array(
+ 'id1' => $conversation,
+ 'id2' => $row['content_id'],
+ 'id1_type' => 'core_message_topics',
+ 'id1_from_parent' => FALSE,
+ 'id2_type' => 'core_message_posts',
+ 'id2_from_parent' => FALSE,
+ 'location_key' => 'core_Messaging'
+ );
+
+ $ext = explode( '.', $row['filename'] );
+ $ext = array_pop( $ext );
+
+ $info = array(
+ 'attach_id' => $row['attachment_id'],
+ 'attach_file' => $row['filename'],
+ 'attach_date' => $row['upload_date'],
+ 'attach_member_id' => $row['user_id'],
+ 'attach_hits' => $row['view_count'],
+ 'attach_ext' => $ext,
+ 'attach_filesize' => $row['file_size'],
+ );
+
+ $physicalName = $row['data_id'] . '-' . $row['file_hash'] . '.data';
+ $group = floor( $row['data_id'] / 1000 );
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $group . '/' . $physicalName;
+
+ $attachId = $libraryClass->convertAttachment( $info, $map, $path );
+
+ /* Update Message Post if we can */
+ try
+ {
+ if ( $attachId !== FALSE )
+ {
+ $ourAttachment = \IPS\File::get( 'core_Attachment', \IPS\Db::i()->select( 'attach_location', 'core_attachments', array( 'attach_id=?', $attachId ) )->first() );
+
+ $messagePostId = $this->app->getLink( $row['content_id'], 'core_message_posts' );
+
+ $post = \IPS\Db::i()->select( 'msg_post', 'core_message_posts', array( "msg_id=?", $messagePostId ) )->first();
+
+ if ( preg_match( "/\[ATTACH(.+?)?\]" . $row['attachment_id'] . "\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH(.+?)?\]" . $row['attachment_id'] . "\[\/ATTACH\]/i", '
', $post );
+ }
+ else
+ {
+ $post .= '
';
+ }
+
+ \IPS\Db::i()->update( 'core_message_posts', array( 'msg_post' => $post ), array( "msg_id=?", $messagePostId ) );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \OutOfRangeException $e ) {}
+
+ $libraryClass->setLastKeyValue( $row['attachment_id'] );
+ }
+ }
+
+ /**
+ * Convert emoticons
+ *
+ * @return void
+ */
+ public function convertEmoticons()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'smilie_id' );
+
+ foreach( $this->fetch( 'xf_smilie', 'smilie_id' ) AS $row )
+ {
+ /* We need to figure out where our file lives - if it's remote, then we need to use file_get_contents() and pass the raw data. */
+ $filepath = NULL;
+ $filedata = NULL;
+
+ if ( mb_substr( $row['image_url'], 0, 4 ) == 'http' )
+ {
+ $filedata = file_get_contents( $row['image_url'] );
+ $fileurl = explode( '/', $row['image_url'] );
+ $filename = array_pop( $fileurl );
+ }
+ else
+ {
+ $fileurl = explode( '/', $row['image_url'] );
+ $filename = array_pop( $fileurl );
+ $filepath = rtrim( $this->app->_session['more_info']['convertEmoticons']['emoticon_path'], '/' ) . '/' . implode( '/', $fileurl );
+ }
+
+ /* XenForo allows multiple codes - we don't. */
+ $code = explode( "\n", $row['smilie_text'] );
+ $code = array_shift( $code );
+
+ /* And our set */
+ try
+ {
+ $category = $this->db->select( '*', 'xf_smilie_category', array( "smilie_category_id=?", $row['smilie_category_id'] ) )->first();
+ $title = $this->getPhrase( "smilie_category_{$category['smilie_category_id']}_title" );
+
+ if ( is_null( $title ) )
+ {
+ /* Bubble Up */
+ throw new \UnderflowException;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $category = array(
+ 'display_order' => 1,
+ );
+
+ $title = "Converted";
+ }
+
+ $set = array(
+ 'set' => md5( $title ),
+ 'title' => $title,
+ 'position' => $category['display_order']
+ );
+
+ $info = array(
+ 'id' => $row['smilie_id'],
+ 'typed' => $code,
+ 'filename' => $filename,
+ 'emo_position' => $row['display_order'],
+ );
+
+ $libraryClass->convertEmoticon( $info, $set, $this->app->_session['more_info']['convertEmoticons']['keep_existing_emoticons'], $filepath, $filedata );
+
+ $libraryClass->setLastKeyValue( $row['smilie_id'] );
+ }
+ }
+
+ /**
+ * Convert profile fields
+ *
+ * @return void
+ */
+ public function convertProfileFields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'xf_user_field', 'field_id' ) AS $row )
+ {
+ $info = array();
+ $info['pf_id'] = $row['field_id'];
+ $merge = $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] != 'none' ? $this->app->_session['more_info']['convertProfileFields']["map_pfield_{$row['field_id']}"] : NULL;
+ $info['pf_type'] = $this->_fieldMap( $row['field_type'] );
+ $info['pf_name'] = $this->getPhrase( "user_field_{$row['field_id']}" );
+ $info['pf_desc'] = $this->getPhrase( "user_field_{$row['field_id']}_desc" );
+ $info['pf_content'] = ( !in_array( $row['field_type'], array( 'textbox', 'textarea' ) ) ) ? \unserialize( $row['field_choices'] ) : NULL;
+ $info['pf_not_null'] = $row['required'];
+ $info['pf_member_hide'] = ( $row['viewable_profile'] > 0 ) ? 0 : 1;
+ $info['pf_max_input'] = $row['max_length'];
+ $info['pf_member_edit'] = ( in_array( $row['user_editable'], array( 'yes', 'once' ) ) ) ? 0 : 1;
+ $info['pf_position'] = $row['display_order'];
+ $info['pf_show_on_reg'] = ( $row['show_registration'] > 0 ) ? 1 : 0;
+ $info['pf_input_format'] = ( $row['match_type'] == 'regex' AND $row['match_regex'] ) ? '/' . $row['match_regex'] . '/i' : NULL;
+ $info['pf_multiple'] = ( $row['field_type'] == 'multiselect' ) ? 1 : 0;
+
+ $libraryClass->convertProfileField( $info, $merge );
+ }
+ }
+
+ /**
+ * Maps the XenForo field ttype to IPS field type
+ *
+ * @param string XF Field Type
+ * @return string IPS Field Type
+ */
+ protected function _fieldMap( $type )
+ {
+ switch( $type )
+ {
+ case 'select':
+ case 'radio':
+ case 'checkbox':
+ return ucwords( $type );
+ break;
+
+ case 'textarea':
+ return 'TextArea';
+ break;
+
+ case 'multiselect':
+ return 'Select';
+ break;
+
+ default:
+ return 'Text';
+ break;
+ }
+ }
+
+ /**
+ * Convert groups
+ *
+ * @return void
+ */
+ public function convertGroups()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'user_group_id' );
+
+ foreach( $this->fetch( 'xf_user_group', 'user_group_id' ) AS $row )
+ {
+ /* Basic info */
+ $info = array(
+ 'g_id' => $row['user_group_id'],
+ 'g_name' => $row['title'],
+ );
+
+ /* XenForo stores raw CSS to style usernames - we can convert this into a tag with an inline style attribute */
+ $style = str_replace( array( ' ', ' ' ), '', nl2br( $row['username_css'] ) );
+ $info['prefix'] = "";
+ $info['suffix'] = " ";
+
+ /* General Permissions */
+ foreach( $this->db->select( '*', 'xf_permission_entry', array( "user_group_id=?", $row['user_group_id'] ) ) AS $perm )
+ {
+ switch( $perm['permission_id'] )
+ {
+ case 'view':
+ $info['g_view_board'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'deleteOwnPost' :
+ $info['g_delete_own_posts'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'editOwnPost':
+ $info['g_edit_posts'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'postThread':
+ $info['g_post_polls'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'votePoll':
+ $info['g_vote_polls'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'deleteOwnThread':
+ $info['g_delete_own_posts'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+
+ case 'maxRecipients':
+ $info['g_max_mass_pm'] = $perm['permission_value_int'];
+ break;
+
+ case 'bypassFloodCheck':
+ $info['g_avoid_flood'] = ($perm['permission_value'] == 'allow') ? 1 : 0;
+ break;
+ }
+ }
+
+ $merge = ( $this->app->_session['more_info']['convertGroups']["map_group_{$row['user_group_id']}"] != 'none' ) ? $this->app->_session['more_info']['convertGroups']["map_group_{$row['user_group_id']}"] : NULL;
+
+ $libraryClass->convertGroup( $info, $merge );
+
+ $libraryClass->setLastKeyValue( $row['user_group_id'] );
+ }
+ }
+
+ /**
+ * Convert warn reasons
+ *
+ * @return void
+ */
+ public function convertWarnReason()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'warning_definition_id' );
+
+ foreach( $this->fetch( 'xf_warning_definition', 'warning_definition_id' ) AS $row )
+ {
+ $title = $this->getPhrase( "warning_definition_{$row['warning_definition_id']}_title" );
+ $remove = ( $row['expiry_type'] == 'never' ) ? 0 : $row['expiry_default'];
+
+ switch( $row['expiry_type'] )
+ {
+ case 'weeks':
+ $remove = $remove * 7;
+ break;
+
+ case 'months':
+ $remove = $remove * 30;
+ break;
+
+ case 'years':
+ $remove = $remove * 365;
+ break;
+ }
+
+
+ $libraryClass->convertWarnReason( array(
+ 'wr_id' => $row['warning_definition_id'],
+ 'wr_name' => $title,
+ 'wr_points_override' => $row['is_editable'],
+ 'wr_remove' => $remove,
+ 'wr_remove_unit' => 'd',
+ 'wr_remove_override' => $row['is_editable'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['warning_definition_id'] );
+ }
+ }
+
+ /**
+ * Convert members
+ *
+ * @return void
+ */
+ public function convertMembers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'xf_user.user_id' );
+
+ $it = $this->fetch( 'xf_user', 'xf_user.user_id' )
+ ->join( 'xf_user_profile', 'xf_user.user_id = xf_user_profile.user_id' )
+ ->join( 'xf_user_option', 'xf_user.user_id = xf_user_option.user_id' );
+
+ foreach( $it AS $row )
+ {
+ /* Fetch our password. XenForo supports multiple authentication types (XF, vB, SMF, etc.) so let's try and retain these as much as possible. */
+ try
+ {
+ $auth = $this->db->select( 'data', 'xf_user_authenticate', array( "user_id=?", $row['user_id'] ) )->first();
+ $data = \unserialize( $auth );
+ $hash = $data['hash'];
+
+ /* vB, IPB, etc. */
+ $salt = NULL;
+ if ( isset( $data['salt'] ) )
+ {
+ $salt = $data['salt'];
+ }
+
+ /* SMF */
+ if ( isset( $data['username'] ) )
+ {
+ $salt = $data['username'];
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Ut oh... do something random. */
+ $hash = md5( uniqid() );
+ $salt = md5( uniqid() );
+ }
+
+ /* IP Address */
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "user_id=? AND action=?", $row['user_id'], 'register' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "user_id=? AND action=?", $row['user_id'], 'login' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+ }
+
+ /* Last warn */
+ try
+ {
+ $last_warn = $this->db->select( 'warning_date', 'xf_warning', array( "user_id=?", $row['user_id'] ), "warning_id DESC" )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $last_warn = 0;
+ }
+
+ /* PM Count */
+ try
+ {
+ $pm_count = $this->db->select( 'COUNT(*)', 'xf_conversation_master', array( "user_id=?", $row['user_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $pm_count = 0;
+ }
+
+ /* Auto Track */
+ $auto_track = 0;
+ if ( $row['default_watch_state'] == 'watch_email' )
+ {
+ $auto_track = array(
+ 'content' => 1,
+ 'comments' => 1,
+ 'method' => 'immediate',
+ );
+ }
+
+ /* Ban */
+ try
+ {
+ $ban = $this->db->select( 'end_date', 'xf_user_ban', array( "user_id=?", $row['user_id'] ) )->first();
+
+ if ( $ban == 0 )
+ {
+ $ban = -1;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $ban = 0;
+ }
+
+ /* Timezone Verification */
+ try
+ {
+ $timezone = new \DateTimeZone( $row['timezone'] ); # if invalid, this will throw an exception
+ }
+ catch( \Exception $e )
+ {
+ $timezone = 'UTC';
+ }
+
+ /* Main Member Information */
+ $info = array(
+ 'member_id' => $row['user_id'],
+ 'name' => $row['username'],
+ 'email' => $row['email'],
+ 'conv_password' => $hash,
+ 'conv_password_extra' => $salt,
+ 'member_group_id' => $row['user_group_id'],
+ 'joined' => $row['register_date'],
+ 'ip_address' => $ip,
+ 'warn_level' => $row['warning_points'],
+ 'warn_lastwarn' => $last_warn,
+ 'bday_day' => $row['dob_day'],
+ 'bday_month' => $row['dob_month'],
+ 'bday_year' => $row['dob_year'],
+ 'msg_count_new' => $row['conversations_unread'],
+ 'msg_count_total' => $pm_count,
+ 'last_visit' => $row['last_activity'],
+ 'last_activity' => $row['last_activity'],
+ 'auto_track' => $auto_track,
+ 'temp_ban' => $ban,
+ 'mgroup_others' => $row['secondary_group_ids'],
+ 'members_bitoptions' => array(
+ 'view_sigs' => $row['content_show_signature']
+ ),
+ 'pp_setting_count_comments' => 1, # always on for XF
+ 'pp_reputation_points' => $row['like_count'],
+ 'timezone' => $timezone,
+ 'allow_admin_mails' => $row['receive_admin_email'],
+ 'member_title' => $row['custom_title'],
+ 'member_posts' => $row['message_count'],
+ );
+
+ /* Profile Photo */
+ $group = floor( $row['user_id'] / 1000 );
+ $path = rtrim( $this->app->_session['more_info']['convertMembers']['photo_location'], '/' );
+ $filename = NULL;
+ $filepath = NULL;
+ if ( file_exists( $path . '/l/' . $group . '/' . $row['user_id'] . '.jpg' ) )
+ {
+ $filename = $row['user_id'] . '.jpg';
+ $filepath = $path . '/l/' . $group;
+ }
+ else
+ {
+ /* Got a gravatar? */
+ if ( $row['gravatar'] )
+ {
+ $info['pp_gravatar'] = $row['gravatar'];
+ }
+ }
+
+ /* Profile Fields */
+ $pfields = array();
+ foreach( $this->db->select( '*', 'xf_user_field_value', array( "user_id=?", $row['user_id'] ) ) AS $field )
+ {
+ $pfields[ $field['field_id'] ] = $field['field_value'];
+ }
+
+ /* Pseudo Fields */
+ foreach( array( 'homepage', 'location', 'occupation', 'about', 'gender' ) AS $pseudo )
+ {
+ /* Are we retaining? */
+ if ( $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"] == 'no_convert' )
+ {
+ /* No, skip */
+ continue;
+ }
+
+ try
+ {
+ /* We don't actually need this, but we need to make sure the field was created */
+ $fieldId = $this->app->getLink( $pseudo, 'core_pfields_data' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->convertProfileField( array(
+ 'pf_id' => $pseudo,
+ 'pf_name' => $this->app->_session['more_info']['convertMembers']["field_{$pseudo}"],
+ 'pf_desc' => '',
+ 'pf_type' => ($pseudo == 'gender') ? 'Select' : 'Text',
+ 'pf_content' => ($pseudo == 'gender') ? json_encode( array( 'male', 'female' ) ) : '[]',
+ 'pf_member_hide' => 0,
+ 'pf_max_input' => ($pseudo == 'gender') ? 0 : 255,
+ 'pf_member_edit' => 1,
+ 'pf_show_on_reg' => 0,
+ 'pf_admin_only' => 0,
+ ) );
+ }
+
+ $pfields[$pseudo] = $row[$pseudo];
+ }
+
+ $libraryClass->convertMember( $info, $pfields, $filename, $filepath );
+
+ /* Followers */
+ foreach( $this->db->select( '*', 'xf_user_follow', array( "user_id=?", $row['user_id'] ) ) AS $follower )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'core',
+ 'follow_area' => 'members',
+ 'follow_rel_id' => $follower['follow_user_id'],
+ 'follow_rel_id_type' => 'core_members',
+ 'follow_member_id' => $follower['user_id'],
+ ) );
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'xf_warning', array( "content_type=? AND user_id=?", 'user', $row['user_id'] ) ) AS $warning )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warning['warning_id'],
+ 'wl_member' => $warning['user_id'],
+ 'wl_moderator' => $warning['warning_user_id'],
+ 'wl_date' => $warning['warning_date'],
+ 'wl_reason' => $warning['warning_definition_id'],
+ 'wl_points' => $warning['points'],
+ 'wl_note_mods' => $warning['notes'],
+ 'wl_expire_date' => $warning['expiry_date'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['user_id'] );
+ }
+ }
+
+ /**
+ * Convert statuses
+ *
+ * @return void
+ */
+ public function convertStatuses()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profile_post_id' );
+
+ foreach( $this->fetch( 'xf_profile_post', 'profile_post_id' ) AS $row )
+ {
+ /* We have to query for the IP Address */
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+
+ /* Approval State */
+ switch( $row['message_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderated':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ $info = array(
+ 'status_id' => $row['profile_post_id'],
+ 'status_member_id' => $row['profile_user_id'],
+ 'status_date' => $row['post_date'],
+ 'status_content' => $row['message'],
+ 'status_replies' => $row['comment_count'],
+ 'status_author_id' => $row['user_id'],
+ 'status_author_ip' => $ip,
+ 'status_approved' => $approved,
+ );
+
+ $libraryClass->convertStatus( $info );
+
+ $libraryClass->setLastKeyValue( $row['profile_post_id'] );
+ }
+ }
+
+ /**
+ * Convert one or more settings
+ *
+ * @param array $settings Settings to convert
+ * @return void
+ */
+ public function convertSettings( $settings=array() )
+ {
+ foreach( $this->settingsMap() AS $theirs => $ours )
+ {
+ if ( !isset( $values[ $ours ] ) OR $values[ $ours ] == FALSE )
+ {
+ continue;
+ }
+
+ try
+ {
+ $setting = $this->db->select( 'option_value', 'xf_option', array( "option_id=?", $theirs ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ continue;
+ }
+
+ \IPS\Db::i()->update( 'core_sys_conf_settings', array( 'conf_value' => $setting ), array( "conf_key=?", $ours ) );
+ }
+ }
+
+ /**
+ * Convert status replies
+ *
+ * @return void
+ */
+ public function convertStatusReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'profile_post_comment_id' );
+
+ foreach( $this->fetch( 'xf_profile_post_comment', 'profile_post_comment_id' ) AS $row )
+ {
+ /* We need to query for the IP Address */
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+
+ /* Approval State */
+ switch( $row['message_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderated':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ $info = array(
+ 'reply_id' => $row['profile_post_comment_id'],
+ 'reply_status_id' => $row['profile_post_id'],
+ 'reply_member_id' => $row['user_id'],
+ 'reply_date' => $row['comment_date'],
+ 'reply_content' => $row['message'],
+ 'reply_approved' => $approved,
+ 'reply_ip_address' => $ip,
+ );
+
+ $libraryClass->convertStatusReply( $info );
+
+ $libraryClass->setLastKeyValue( $row['profile_post_comment_id'] );
+ }
+ }
+
+ /**
+ * Convert ignored users
+ *
+ * @return void
+ */
+ public function convertIgnoredUsers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'xf_user_ignored', 'user_id' ) AS $row )
+ {
+ $libraryClass->convertIgnoredUser( array(
+ 'ignore_id' => $row['user_id'] . '-' . $row['ignore_user_id'],
+ 'ignore_owner_id' => $row['user_id'],
+ 'ignore_ignore_id' => $row['ignore_user_id']
+ ) );
+ }
+ }
+
+ /**
+ * Convert ranks
+ *
+ * @return void
+ */
+ public function convertRanks()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'minimum_level' );
+
+ foreach( $this->fetch( 'xf_user_title_ladder', 'minimum_level' ) AS $row )
+ {
+ $libraryClass->convertRank( array(
+ 'id' => $row['minimum_level'],
+ 'title' => $row['title'],
+ 'posts' => $row['minimum_level'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['minimum_level'] );
+ }
+ }
+
+ /**
+ * Convert private messages
+ *
+ * @return void
+ */
+ public function convertPrivateMessages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'conversation_id' );
+
+ foreach( $this->fetch( 'xf_conversation_master', 'conversation_id' ) AS $row )
+ {
+ $topic = array(
+ 'mt_id' => $row['conversation_id'],
+ 'mt_date' => $row['start_date'],
+ 'mt_title' => $row['title'],
+ 'mt_starter_id' => $row['user_id'],
+ 'mt_start_time' => $row['start_date'],
+ 'mt_last_post_time' => $row['last_message_date'],
+ 'mt_to_count' => $row['recipient_count'],
+ 'mt_replies' => $row['reply_count'],
+ );
+
+ $maps = array();
+ foreach( $this->db->select( '*', 'xf_conversation_user', array( 'conversation_id=?', $row['conversation_id'] ) ) AS $map )
+ {
+ try
+ {
+ $recip = $this->db->select( '*', 'xf_conversation_recipient', array( "conversation_id=? AND user_id=?", $row['conversation_id'], $map['owner_user_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $recip = array(
+ 'conversation_id' => $row['conversation_id'],
+ 'user_id' => $map['owner_user_id'],
+ 'recipient_state' => 'deleted',
+ 'last_read_date' => time(),
+ );
+ }
+ $maps[$map['owner_user_id']] = array(
+ 'map_user_id' => $map['owner_user_id'],
+ 'map_read_time' => $recip['last_read_date'],
+ 'map_user_active' => ( $recip['recipient_state'] == 'active' ) ? 1 : 0,
+ 'map_user_banned' => ( $recip['recipient_state'] == 'deleted_ignored' ) ? 1 : 0,
+ 'map_has_unread' => $map['is_unread'],
+ 'map_is_starter' => ( $map['owner_user_id'] == $row['user_id'] ) ? 1 : 0,
+ 'map_last_topic_reply' => $map['last_message_date']
+ );
+ }
+
+ $libraryClass->convertPrivateMessage( $topic, $maps );
+
+ $libraryClass->setLastKeyValue( $row['conversation_id'] );
+ }
+ }
+
+ /**
+ * Convert private message replies
+ *
+ * @return void
+ */
+ public function convertPrivateMessageReplies()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'message_id' );
+ $ips = array();
+ foreach( $this->fetch( 'xf_conversation_message', 'message_id' ) AS $row )
+ {
+ if ( !isset( $ips[$row['ip_id'] ] ) )
+ {
+ try
+ {
+ $ips[$row['ip_id']] = $this->db->select( 'ip', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ips[$row['ip_id']] = '127.0.0.1';
+ }
+ }
+
+ $libraryClass->convertPrivateMessageReply( array(
+ 'msg_id' => $row['message_id'],
+ 'msg_topic_id' => $row['conversation_id'],
+ 'msg_date' => $row['message_date'],
+ 'msg_post' => $row['message'],
+ 'msg_author_id' => $row['user_id'],
+ 'msg_ip_address' => $ips[$row['ip_id']],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['message_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ * @todo The proxy.php redirection is not validating the hash that XenForo set previously, derived from the globalSalt and the option imageLinkProxyKey. We need
+ * to either validate the hash (there is an open proxy concern), or we need to convert all calls to proxy.php in posted content to prevent broken images in posts.
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/members/(.+?)\.([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ /* If we can't access profiles, don't bother trying to redirect */
+ if( !\IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'members' ) ) )
+ {
+ return NULL;
+ }
+
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[2], array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'proxy.php' ) !== FALSE AND isset( \IPS\Request::i()->image ) )
+ {
+ return \IPS\Http\Url::internal( "applications/core/interface/imageproxy/imageproxy.php", 'none' )->setQueryString( array(
+ 'img' => \IPS\Request::i()->image,
+ 'key' => hash_hmac( "sha256", (string) \IPS\Request::i()->image, \IPS\Settings::i()->site_secret_key )
+ ) );
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Downloads/Xenforo.php b/applications/convert/sources/Software/Downloads/Xenforo.php
new file mode 100644
index 00000000..2c75eed5
--- /dev/null
+++ b/applications/convert/sources/Software/Downloads/Xenforo.php
@@ -0,0 +1,491 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Downloads;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenforo extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "XenForo Resource Manager";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "xenforo";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertDownloadsCfields' => array(
+ 'table' => 'xf_resource_field',
+ 'where' => NULL
+ ),
+ 'convertDownloadsCategories' => array(
+ 'table' => 'xf_resource_category',
+ 'where' => NULL
+ ),
+ 'convertDownloadsFiles' => array(
+ 'table' => 'xf_resource',
+ 'where' => array( "is_fileless=?", 0 )
+ ),
+ 'convertDownloadsReviews' => array(
+ 'table' => 'xf_resource_rating',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'xenforo' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertDownloadsFiles',
+ 'convertDownloadsReviews'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertDownloadsFiles':
+ $return['convertDownloadsFiles']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+
+ case 'convertDownloadsReviews':
+ $return['convertDownloadsReviews']['convert_author_replies_to_comments'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\YesNo',
+ 'field_default' => FALSE,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ );
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase
+ * @return string|null
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Convert custom fields
+ *
+ * @return void
+ */
+ public function convertDownloadsCfields()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach( $this->fetch( 'xf_resource_field', 'field_id' ) AS $row )
+ {
+ $info = array(
+ 'cf_id' => $row['field_id'],
+ 'cf_type' => $this->_mapFieldType( $row['field_type'], $row['match_type'] ),
+ 'cf_content' => \unserialize( $row['field_choices'] ),
+ 'cf_not_null' => $row['required'],
+ 'cf_max_input' => $row['max_length'],
+ 'cf_input_format' => ( $row['match_type'] == 'regex' ) ? '/' . $row['match_regex'] .'/i' : '',
+ 'cf_position' => $row['display_order'],
+ 'cf_multiple' => ( in_array( $row['field_type'], array( 'checkbox', 'multiselect' ) ) ) ? 1 : 0
+ );
+
+ $libraryClass->convertDownloadsCfield( $info );
+ }
+ }
+
+ /**
+ * Map the field types
+ *
+ * @return void
+ */
+ protected function _mapFieldType( $type, $match )
+ {
+ switch( $type )
+ {
+ case 'textbox':
+ /* Yay, nested switches */
+ switch( $match )
+ {
+ case 'email':
+ return 'Email';
+ break;
+
+ case 'url':
+ return 'Url';
+ break;
+
+ case 'number':
+ return 'Number';
+ break;
+
+ default:
+ return 'Text';
+ break;
+ }
+ break;
+
+ case 'textarea':
+ return 'TextArea';
+ break;
+
+ case 'bbcode':
+ return 'Editor';
+ break;
+
+ case 'multiselect':
+ case 'select':
+ return 'Select';
+ break;
+
+ case 'checkbox':
+ return 'CheckboxSet';
+ break;
+ }
+ }
+
+ /**
+ * Convert categories
+ *
+ * @return void
+ */
+ public function convertDownloadsCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'resource_category_id' );
+
+ foreach( $this->fetch( 'xf_resource_category', 'resource_category_id' ) AS $row )
+ {
+ /* Custom Fields for this category */
+ $ccfields = array();
+ foreach( $this->db->select( 'field_id', 'xf_resource_field_category', array( "resource_category_id=?", $row['resource_category_id'] ) ) AS $field )
+ {
+ $ccfields[] = $field;
+ }
+
+ /* Topic Prefix */
+ $prefix = '';
+ if ( $row['thread_prefix_id'] AND $row['thread_node_id'] )
+ {
+ $prefix = $this->getPhrase( "thread_prefix_{$row['thread_prefix_id']}" );
+
+ if ( is_null( $prefix ) )
+ {
+ $prefix = '';
+ }
+ }
+
+ $info = array(
+ 'cid' => $row['resource_category_id'],
+ 'cparent' => $row['parent_category_id'],
+ 'cname' => $row['category_title'],
+ 'cdesc' => $row['category_description'],
+ 'copen' => ( $row['allow_local'] OR $row['allow_external'] ) ? 1 : 0,
+ 'cposition' => $row['display_order'],
+ 'ccfields' => $ccfields,
+ 'cforum_id' => $row['thread_node_id'],
+ 'ctopic_prefix' => $prefix
+ );
+
+ $libraryClass->convertDownloadsCategory( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xf_resource_category_watch', array( "notify_on=? AND resource_category_id=?", 'resource', $row['resource_category_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'downloads',
+ 'follow_area' => 'category',
+ 'follow_rel_id' => $row['resource_category_id'],
+ 'follow_rel_id_type' => 'downloads_categories',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['send_alert'] OR $follow['send_email'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['resource_category_id'] );
+ }
+ }
+
+ /**
+ * Convert files
+ *
+ * @return void
+ */
+ public function convertDownloadsFiles()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'resource_id' );
+
+ foreach( $this->fetch( 'xf_resource', 'resource_id', array( "is_fileless=?", 0 ) ) AS $row )
+ {
+ /* Latest Version Stuff */
+ $latest = $this->db->select( '*', 'xf_resource_update', array( "xf_resource_update.resource_id=?", $row['resource_id'] ), "xf_resource_update.post_date DESC" )
+ ->join( 'xf_resource_version', "xf_resource_update.resource_update_id = xf_resource_version.resource_update_id" )
+ ->first();
+
+ $latest_version = $latest['version_string'];
+ $latest_changelog = $latest['message'];
+
+ /* Featured */
+ $featured = 0;
+ try
+ {
+ $this->db->select( '*', 'xf_resource_feature', array( "resource_id=?", $row['resource_id'] ) )->first();
+
+ $featured = 1;
+ }
+ catch( \UnderflowExceptioN $e ) {}
+
+ $info = array(
+ 'file_id' => $row['resource_id'],
+ 'file_desc' => $row['tag_line'],
+ 'file_name' => $row['title'],
+ 'file_cat' => $row['resource_category_id'],
+ 'file_downloads' => $this->db->select( 'COUNT(*)', 'xf_resource_download', array( 'resource_id=?', $row['resource_id'] ) )->first(),
+ 'file_reviews' => $row['review_count'],
+ 'file_submitted' => $row['resource_date'],
+ 'file_updated' => $row['last_update'],
+ 'file_submitter' => $row['user_id'],
+ 'file_topicid' => $row['discussion_thread_id'],
+ 'file_version' => $latest_version,
+ 'file_changelog' => $latest_changelog,
+ 'file_featured' => $featured,
+ );
+
+ $records = array();
+ $recordit = $this->db->select( '*', 'xf_resource_update', array( "xf_resource_update.resource_id=?", $row['resource_id'] ) )->join( 'xf_resource_version', "xf_resource_update.resource_update_id = xf_resource_version.resource_update_id" );
+ foreach( $recordit AS $record )
+ {
+ if ( $record['download_url'] )
+ {
+ $records[] = array(
+ 'record_id' => $record['resource_update_id'],
+ 'record_type' => 'link',
+ 'file_path' => $record['download_url'],
+ 'record_time' => $record['release_date']
+ );
+ }
+ else
+ {
+ $file_data = $this->db->select( '*', 'xf_attachment', array( "xf_attachment.content_type=? AND xf_attachment.content_id=?", 'resource_version', $record['resource_version_id'] ) )->join( 'xf_attachment_data', "xf_attachment.data_id = xf_attachment_data.data_id" )->first();
+ $group = floor( $row['user_id'] / 1000 );
+
+ $records[] = array(
+ 'record_id' => $record['resource_update_id'],
+ 'record_type' => 'upload',
+ 'file_path' => rtrim( $this->app->_session['more_info']['convertDownloadsFiles']['file_location'], '/' ) . '/' . $group . '/' . $file_data['data_id'] . '-' . $file_data['file_hash'] . '.data',
+ 'record_time' => $record['release_date']
+ );
+ }
+
+ /* And now screenshots */
+ foreach( $this->db->select( '*', 'xf_attachment', array( "xf_attachment.content_type=? AND xf_attachment.content_id=?", 'resource_update', $record['resource_update_id'] ) )->join( 'xf_attachment_data', "xf_attachment.data_id = xf_attachment_data.data_id" ) AS $screen )
+ {
+ $group = floor( $row['user_id'] / 1000 );
+
+ $records[] = array(
+ 'record_type' => 'ssupload',
+ 'file_path' => rtrim( $this->app->_session['more_info']['convertDownloadsFiles']['file_location'], '/' ) . '/' . $group . '/' . $screen['data_id'] . '-' . $screen['file_hash'] . '.data',
+ 'record_time' => $record['release_date']
+ );
+ }
+ }
+
+ $cfields = array();
+ foreach( $this->db->select( 'field_id, field_value', 'xf_resource_field_value', array( "resource_id=?", $row['resource_id'] ) )->setKeyField( 'field_id' )->setValueField( 'field_value' ) AS $key => $value )
+ {
+ $field_type = $this->db->select( 'field_type', 'xf_resource_field', array( "field_id=?", $key ) )->first();
+
+ if ( in_array( $field_type, array( 'checkbox', 'multiselect' ) ) )
+ {
+ $cfields[$key] = json_encode( \unserialize( $value ) );
+ }
+ else
+ {
+ $cfields[$key] = $value;
+ }
+ }
+
+ $libraryClass->convertDownloadsFile( $info, $records, $cfields );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xf_resource_watch', array( "resource_id=?", $row['resource_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'downloads',
+ 'follow_area' => 'file',
+ 'follow_rel_id' => $row['resource_id'],
+ 'follow_rel_id_type' => 'downloads_files',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['email_subscribe'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['resource_id'] );
+ }
+ }
+
+ /**
+ * Convert reviews
+ *
+ * @return void
+ */
+ public function convertDownloadsReviews()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'resource_rating_id' );
+
+ foreach( $this->fetch( 'xf_resource_rating', 'resource_rating_id' ) AS $row )
+ {
+ switch( $row['rating_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderated':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ $info = array(
+ 'review_id' => $row['resource_rating_id'],
+ 'review_text' => $row['message'],
+ 'review_rating' => $row['rating'],
+ 'review_mid' => $row['user_id'],
+ 'review_fid' => $row['resource_id'],
+ 'review_date' => $row['rating_date'],
+ 'review_approved' => $approved,
+ 'review_version' => $row['version_string']
+ );
+
+ $libraryClass->convertDownloadsReview( $info );
+
+ if ( $this->app->_session['more_info']['convertDownloadsReviews']['convert_author_replies_to_comments'] AND $row['author_response'] )
+ {
+ /* Get Resource */
+ $resource = $this->db->select( '*', 'xf_resource', array( "resource_id=?", $row['resource_id'] ) )->first();
+ $member = $this->db->select( '*', 'xf_user', array( "user_id=?", $row['user_id'] ) )->first();
+ $post = "{$row['message']} {$row['author_response']}
";
+
+ $libraryClass->convertDownloadsComment( array(
+ 'comment_id' => $row['resource_rating_id'],
+ 'comment_fid' => $row['resource_id'],
+ 'comment_text' => $post,
+ 'comment_mid' => $resource['user_id'],
+ 'comment_date' => $row['rating_date'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['resource_rating_id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Exception.php b/applications/convert/sources/Software/Exception.php
new file mode 100644
index 00000000..990de469
--- /dev/null
+++ b/applications/convert/sources/Software/Exception.php
@@ -0,0 +1,22 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Exception extends \OutOfRangeException
+{}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Bbpress.php b/applications/convert/sources/Software/Forums/Bbpress.php
new file mode 100644
index 00000000..bd81575c
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Bbpress.php
@@ -0,0 +1,219 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Nov 2016
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Bbpress extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "BBPress (for WordPress)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "bbpress";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'posts',
+ 'where' => array( 'post_type=?', 'forum' ),
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'posts',
+ 'where' => array( 'post_type=?', 'topic' ),
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'posts',
+ 'where' => array( '(post_type=? OR post_type=?)', 'topic', 'reply' ),
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'wordpress' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'ID' );
+
+ foreach( $this->fetch( 'posts', 'ID', array( 'post_type=?', 'forum' ) ) AS $row )
+ {
+ $info = array(
+ 'id' => $row['ID'],
+ 'name' => $row['post_title'],
+ 'description' => $row['post_content'],
+ 'parent_id' => $row['post_parent'],
+ );
+
+ $libraryClass->convertForumsForum( $info );
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+
+ /**
+ * User cache to minimise repeated DB lookups
+ *
+ * @var array $userCache
+ */
+ protected static $userCache = array();
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'ID' );
+
+ /* Which username type? */
+ $userName = $this->app->_parent->_session['more_info']['convertMembers']['username'] == 'username' ? 'user_login' : 'display_name';
+
+ foreach( $this->fetch( 'posts', 'ID', array( 'post_type=?', 'topic' ) ) AS $row )
+ {
+ if( !isset( static::$userCache[ $row['post_author'] ] ) )
+ {
+ try
+ {
+ static::$userCache[ $row['post_author'] ] = $this->db->select( $userName, 'users', array( 'ID=?', $row['post_author'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ static::$userCache[ $row['post_author'] ] = 'Guest';
+ }
+ }
+
+ $info = array(
+ 'tid' => $row['ID'],
+ 'title' => $row['post_title'],
+ 'forum_id' => $row['post_parent'],
+ 'state' => ( $row['post_status'] == 'publish' ) ? 'open' : 'closed',
+ 'starter_id' => $row['post_author'],
+ 'start_date' => \strtotime( $row['post_date'] ),
+ 'starter_name' => static::$userCache[ $row['post_author'] ],
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'ID' );
+
+ /* Which username type? */
+ $userName = $this->app->_parent->_session['more_info']['convertMembers']['username'] == 'username' ? 'user_login' : 'display_name';
+
+ foreach( $this->fetch( 'posts', 'ID', array( '(post_type=? OR post_type=?)', 'topic', 'reply' ) ) AS $row )
+ {
+ if( !isset( static::$userCache[ $row['post_author'] ] ) )
+ {
+ try
+ {
+ static::$userCache[ $row['post_author'] ] = $this->db->select( $userName, 'users', array( 'ID=?', $row['post_author'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ static::$userCache[ $row['post_author'] ] = 'Guest';
+ }
+ }
+
+ $info = array(
+ 'pid' => $row['ID'],
+ 'topic_id' => $row['post_type'] == 'topic' ? $row['ID'] : $row['post_parent'],
+ 'post' => $row['post_content'],
+ 'author_id' => $row['post_author'],
+ 'author_name' => static::$userCache[ $row['post_author'] ],
+ 'post_date' => \strtotime( $row['post_date'] )
+ );
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['ID'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Expressionengine.php b/applications/convert/sources/Software/Forums/Expressionengine.php
new file mode 100644
index 00000000..dc46e325
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Expressionengine.php
@@ -0,0 +1,498 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 17 June 2016
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Expressionengine extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "ExpressionEngine";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "expressionengine";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'exp_forums',
+ 'where' => NULL
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'exp_forum_topics',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'exp_forum_topics',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertOtherPosts' )
+ ),
+ 'convertOtherPosts' => array(
+ 'table' => 'exp_forum_posts',
+ 'where' => NULL,
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'expForumAttachments',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'expressionengine' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ee_forum_at_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return string Message to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Expressionengine::fixPostData( $post );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'forum_id' );
+
+ foreach( $this->fetch( 'exp_forums', 'forum_id' ) AS $row )
+ {
+ $info = array(
+ 'id' => $row['forum_id'],
+ 'name' => $row['forum_name'],
+ 'description' => $row['forum_description'],
+ 'topics' => $row['forum_total_topics'],
+ 'posts' => $row['forum_total_posts'],
+ 'last_post' => $row['forum_last_post_date'],
+ 'last_poster_id' => $row['forum_last_post_author_id'],
+ 'last_poster_name' => $row['forum_last_post_author'],
+ 'parent_id' => ( $row['forum_parent'] == 0 ) ? -1 : $row['forum_parent'],
+ 'position' => $row['forum_order'],
+ 'last_title' => $row['forum_last_post_title'],
+ 'allow_poll' => 1,
+ 'inc_postcount' => 1,
+ 'sub_can_post' => 1,
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ $libraryClass->setLastKeyValue( $row['forum_id'] );
+ }
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'topic_id' );
+
+ foreach( $this->fetch( 'exp_forum_topics', 'topic_id' )->join( 'exp_members', 'exp_members.member_id = exp_forum_topics.author_id' ) AS $row )
+ {
+ /* Poll */
+ $poll = NULL;
+
+ if( $row['poll'] == 'y' )
+ {
+ $choices = array();
+ $votes = array();
+ $index = 1;
+
+ $pollData = $this->db->select( '*', 'exp_forum_polls', array( 'topic_id=?', $row['topic_id'] ) )->first();
+
+ foreach( \unserialize( $pollData['poll_answers'] ) as $value )
+ {
+ $choices[ $index ] = $value['answer'];
+ $votes[ $index ] = $value['votes'];
+ $index++;
+ }
+
+ $poll['poll_data'] = array(
+ 'pid' => $pollData['poll_id'],
+ 'choices' => array(
+ 1 => array(
+ 'question' => $pollData['poll_question'],
+ 'multi' => 0,
+ 'choice' => $choices,
+ 'votes' => $votes
+ )
+ ),
+ 'poll_question' => $pollData['poll_question'],
+ 'start_date' => $pollData['poll_date'],
+ 'starter_id' => $pollData['author_id'],
+ 'votes' => array_sum( $votes ),
+ 'poll_view_voters' => 0
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'exp_forum_pollvotes', array( "poll_id=?", $pollData['poll_id'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[ $vote['member_id'] ] ) )
+ {
+ $ourVotes[ $vote['member_id'] ] = array( 'votes' => array() );
+ }
+
+ $ourVotes[ $vote['member_id'] ]['votes'][] = ( $vote['choice_id'] + 1 );
+ $ourVotes[ $vote['member_id'] ]['member_id'] = $vote['member_id'];
+ }
+
+ foreach( $ourVotes AS $memberId => $vote )
+ {
+ $poll['vote_data'][ $memberId ] = array(
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+
+ $lastPoster = NULL;
+ try
+ {
+ if( $row['last_post_author_id'] )
+ {
+ $lastPoster = $this->db->select( 'screen_name', 'exp_members', array( 'member_id=?', $row['last_post_author_id'] ) )->first();
+ }
+ }
+ catch( \UnderflowException $e ) { }
+
+ $info = array(
+ 'tid' => $row['topic_id'],
+ 'title' => $row['title'],
+ 'forum_id' => $row['forum_id'],
+ 'state' => ( $row['status'] == 'o' ) ? 'open' : 'closed',
+ 'posts' => $row['thread_total'],
+ 'starter_id' => $row['author_id'],
+ 'start_date' => $row['topic_date'],
+ 'last_poster_id' => $row['last_post_author_id'],
+ 'last_post' => $row['last_post_date'],
+ 'starter_name' => $row['screen_name'],
+ 'last_poster_name' => $lastPoster,
+ 'poll_state' => $poll,
+ 'views' => $row['thread_views'],
+ 'approved' => 1,
+ 'pinned' => ( $row['sticky'] == 'y' ) ? 1 : 0
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ $libraryClass->setLastKeyValue( $row['topic_id'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'topic_id' );
+
+ foreach( $this->fetch( 'exp_forum_topics', 'topic_id', NULL, 'exp_forum_topics.*, exp_members.screen_name' )
+ ->join( 'exp_members', 'exp_members.member_id = exp_forum_topics.author_id' ) AS $row )
+ {
+ $editName = NULL;
+ try
+ {
+ if( $row['topic_edit_date'] )
+ {
+ $editName = $this->db->select( 'screen_name', 'exp_members', array( 'member_id=?', $row['topic_edit_author'] ) )->first();
+ }
+ }
+ catch( \UnderflowException $e ) { }
+
+ $info = array(
+ 'pid' => 't' . $row['topic_id'],
+ 'topic_id' => $row['topic_id'],
+ 'post' => $this->fixPostData( $row['body'] ),
+ 'edit_time' => $row['topic_edit_date'],
+ 'edit_name' => $editName,
+ 'author_id' => $row['author_id'],
+ 'author_name' => $row['screen_name'],
+ 'ip_address' => $row['ip_address'],
+ 'post_date' => $row['topic_date'],
+ 'queued' => 0
+ );
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['topic_id'] );
+ }
+ }
+
+ /**
+ * Convert other posts
+ *
+ * @return void
+ */
+ public function convertOtherPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'post_id' );
+
+ foreach( $this->fetch( 'exp_forum_posts', 'post_id', NULL, 'exp_forum_posts.*, exp_members.screen_name' )
+ ->join( 'exp_members', 'exp_members.member_id = exp_forum_posts.author_id' ) AS $row )
+ {
+ $editName = NULL;
+ try
+ {
+ if( $row['post_edit_date'] )
+ {
+ $editName = $this->db->select( 'screen_name', 'exp_members', array( 'member_id=?', $row['post_edit_author'] ) )->first();
+ }
+ }
+ catch( \UnderflowException $e ) { }
+
+ $info = array(
+ 'pid' => $row['post_id'],
+ 'topic_id' => $row['topic_id'],
+ 'post' => $this->fixPostData( $row['body'] ),
+ 'edit_time' => $row['post_edit_date'],
+ 'edit_name' => $editName,
+ 'author_id' => $row['author_id'],
+ 'author_name' => $row['screen_name'],
+ 'ip_address' => $row['ip_address'],
+ 'post_date' => $row['post_date'],
+ 'queued' => 0
+ );
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['post_id'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'attachment_id' );
+
+ $it = $this->fetch( 'exp_forum_attachments', 'attachment_id' );
+
+ foreach( $it AS $row )
+ {
+ $map = array(
+ 'id1' => $row['topic_id'],
+ 'id2' => $row['post_id'],
+ );
+
+ $info = array(
+ 'attach_id' => $row['attachment_id'],
+ 'attach_file' => $row['filename'],
+ 'attach_date' => $row['attachment_date'],
+ 'attach_member_id' => $row['member_id'],
+ 'attach_hits' => $row['hits'],
+ 'attach_ext' => \strtolower( trim( $row['extension'], '.' ) ),
+ 'attach_filesize' => ( $row['filesize'] * 1000 ), //convert to kb
+ );
+
+ $realName = $row['filehash'] . $row['extension'];
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $realName;
+
+ $libraryClass->convertAttachment( $info, $map, $path );
+
+ $libraryClass->setLastKeyValue( $row['attachment_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/(viewforum|viewthread|viewreply)/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ switch( $matches[1] )
+ {
+ case 'viewforum':
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ break;
+
+ case 'viewthread':
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ break;
+
+ case 'viewreply':
+ $class = '\IPS\forums\Topic\Post';
+ $types = array( 'posts', 'forums_posts' );
+ break;
+ }
+
+ /* Sanity check - we found one right? */
+ if( !isset( $class ) )
+ {
+ return NULL;
+ }
+
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[2], $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( (int) $matches[2], $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Mybb.php b/applications/convert/sources/Software/Forums/Mybb.php
new file mode 100644
index 00000000..9962feb0
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Mybb.php
@@ -0,0 +1,585 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Mybb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "MyBB 1.8.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "mybb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'forums',
+ 'where' => NULL
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'threads',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'posts',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'attachments',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'mybb' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_mybb_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'forums_topics', 'class' => 'IPS\forums\Topic' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics", "f_topic_tags_recount" );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'fid' );
+
+ foreach( $this->fetch( 'forums', 'fid' ) AS $row )
+ {
+ $info = array(
+ 'id' => $row['fid'],
+ 'name' => $row['name'],
+ 'description' => $row['description'],
+ 'topics' => $row['threads'],
+ 'posts' => $row['posts'],
+ 'last_post' => $row['lastpost'],
+ 'last_poster_id' => $row['lastposteruid'],
+ 'last_poster_name' => $row['lastposter'],
+ 'parent_id' => $row['pid'] ?: -1,
+ 'position' => $row['disporder'],
+ 'password' => $row['password'] ?: NULL,
+ 'last_title' => $row['lastpostsubject'],
+ 'inc_postcount' => $row['usepostcounts'],
+ 'redirect_url' => $row['linkto'],
+ 'sub_can_post' => ( $row['type'] == 'c' ) ? 0 : 1,
+ 'queued_topics' => $row['unapprovedthreads'],
+ 'queued_posts' => $row['unapprovedposts'],
+ 'forum_allow_rating' => $row['allowtratings'],
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ /* Followers */
+ foreach( $this->db->select( '*', 'forumsubscriptions', array( "fid=?", $row['fid'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'forum',
+ 'follow_rel_id' => $row['fid'],
+ 'follow_rel_id_type' => 'forums_forums',
+ 'follow_member_id' => $follow['uid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['fid'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'tid' );
+
+ foreach( $this->fetch( 'threads', 'tid' ) AS $row )
+ {
+ /* Poll */
+ $poll = NULL;
+ if ( $row['poll'] > 0 )
+ {
+ try
+ {
+ $pollData = $this->db->select( '*', 'polls', array( "pid=?", $row['poll'] ) )->first();
+
+ $choices = array();
+ $index = 1;
+ foreach( explode( '||~|~||', $pollData['options'] ) AS $choice )
+ {
+ $choices[$index] = trim( $choice );
+ $index++;
+ }
+
+ /* Reset Index */
+ $index = 1;
+ $votes = array();
+ $numvotes = 0;
+ foreach( explode( '||~|~||', $pollData['votes'] ) AS $vote )
+ {
+ $count = $this->db->select( 'COUNT(*)', 'pollvotes', array( "pid=? AND voteoption=?", $row['poll'], $index ) )->first();
+ $votes[$index] = $count;
+ $index++;
+
+ $numvotes += $count;
+ }
+
+ $poll = array();
+ $poll['poll_data'] = array(
+ 'pid' => $pollData['pid'],
+ 'choices' => array( 1 => array(
+ 'question' => $pollData['question'],
+ 'multi' => $pollData['multiple'],
+ 'choice' => $choices,
+ 'votes' => $votes
+ ) ),
+ 'poll_question' => $pollData['question'],
+ 'start_date' => $pollData['dateline'],
+ 'starter_id' => $row['uid'],
+ 'votes' => $numvotes,
+ 'poll_view_voters' => $pollData['public']
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'pollvotes', array( "pid=?", $pollData['pid'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[$vote['uid']] ) )
+ {
+ /* Create our structure - vB stores each individual vote as a different row whereas we combine them per user */
+ $ourVotes[$vote['uid']] = array( 'votes' => array() );
+ }
+
+ $ourVotes[$vote['uid']]['votes'][] = $vote['voteoption'];
+
+ /* These don't matter - just use the latest one */
+ $ourVotes[$vote['uid']]['vid'] = $vote['vid'];
+ $ourVotes[$vote['uid']]['vote_date'] = $vote['dateline'];
+ $ourVotes[$vote['uid']]['member_id'] = $vote['uid'];
+ }
+
+ /* Now we need to re-wrap it all for storage */
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][$member_id] = array(
+ 'vid' => $vote['vid'],
+ 'vote_date' => $vote['vote_date'],
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+ catch( \UnderflowException $e ) {} # if the poll is missing, don't bother
+ }
+
+ /* Moved ?*/
+ $moved = explode( "|", $row['closed'] );
+ $moved_to = NULL;
+ if ( isset( $moved[0] ) AND $moved[0] == 'moved' )
+ {
+ try
+ {
+ $moved_to = array(
+ $moved[1],
+ $this->db->select( 'fid', 'threads', array( "tid=?", $moved[1] ) )->first()
+ );
+ }
+ catch( \UnderflowException $e )
+ {
+ $moved_to = NULL;
+ }
+ }
+
+ $info = array(
+ 'tid' => $row['tid'],
+ 'title' => $row['subject'],
+ 'forum_id' => $row['fid'],
+ 'state' => ( $row['closed'] == 1 ) ? 'closed' : 'open',
+ 'starter_id' => $row['uid'],
+ 'start_date' => $row['dateline'],
+ 'last_poster_id' => $row['lastposteruid'],
+ 'last_post' => $row['lastpost'],
+ 'starter_name' => $row['username'],
+ 'last_poster_name' => $row['lastposter'],
+ 'poll_state' => $poll,
+ 'views' => $row['views'],
+ 'approved' => $row['visible'], # it's handled exactly the same
+ 'pinned' => $row['sticky'],
+ 'moved_to' => $moved_to,
+ 'topic_queuedposts' => $row['unapprovedposts'],
+ 'topic_rating_total' => $row['totalratings'],
+ 'topic_rating_hits' => $row['numratings'],
+ 'topic_hiddenposts' => $row['deletedposts']
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ /* If we have a prefix, convert it */
+ if ( $row['prefix'] > 0 )
+ {
+ try
+ {
+ $prefix = $this->db->select( 'prefix', 'threadprefixes', array( "pid=?", $row['prefix'] ) )->first();
+
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $row['fid'],
+ 'tag_meta_id' => $row['tid'],
+ 'tag_text' => $prefix,
+ 'tag_member_id' => $row['uid'],
+ 'tag_prefix' => 1,
+ ) );
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'threadsubscriptions', array( "tid=?", $row['tid'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['tid'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['uid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ /* Ratings */
+ foreach( $this->db->select( '*', 'threadratings', array( "tid=?", $row['tid'] ) ) AS $rating )
+ {
+ $libraryClass->convertRating( array(
+ 'id' => $rating['rid'],
+ 'class' => 'IPS\\forums\\Topic',
+ 'item_link' => 'forums_topics',
+ 'item_id' => $rating['tid'],
+ 'ip' => $rating['ipaddress'],
+ 'rating' => $rating['rating'],
+ 'member' => $rating['uid']
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['tid'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pid' );
+
+ foreach( $this->fetch( 'posts', 'pid' ) AS $row )
+ {
+ switch( $row['visible'] )
+ {
+ case 1:
+ $queued = 0;
+ break;
+
+ case 0:
+ $queued = 1;
+ break;
+
+ case -1:
+ $queued = -1;
+ break;
+ }
+
+ $info = array(
+ 'pid' => $row['pid'],
+ 'topic_id' => $row['tid'],
+ 'post' => $row['message'],
+ 'edit_time' => $row['edittime'],
+ 'author_id' => $row['uid'],
+ 'author_name' => $row['username'],
+ 'ip_address' => $row['ipaddress'],
+ 'post_date' => $row['dateline'],
+ 'queued' => $queued,
+ 'post_edit_reason' => $row['editreason'],
+ );
+
+ $libraryClass->convertForumsPost( $info );
+
+ /* Reputation */
+ foreach( $this->db->select( '*', 'reputation', array( "pid=?", $row['pid'] ) ) AS $rep )
+ {
+ $libraryClass->convertReputation( array(
+ 'id' => $rep['rid'],
+ 'app' => 'forums',
+ 'type' => 'pid',
+ 'type_id' => $row['pid'],
+ 'member_id' => $rep['adduid'],
+ 'member_received' => $rep['uid'],
+ 'rep_date' => $rep['dateline'],
+ 'rep_rating' => $rep['reputation']
+ ) );
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'warnings', array( "pid=?", $row['pid'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['wid'],
+ 'wl_member' => $warn['uid'],
+ 'wl_moderator' => $warn['issuedby'],
+ 'wl_date' => $warn['dateline'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_mods' => $warn['notes'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['pid'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'aid' );
+
+ foreach( $this->fetch( 'attachments', 'aid' ) AS $row )
+ {
+ try
+ {
+ $topic_id = $this->db->select( 'tid', 'posts', array( "pid=?", $row['pid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Post is orphaned */
+ $libraryClass->setLastKeyValue( $row['aid'] );
+ continue;
+ }
+
+ $map = array(
+ 'id1' => $topic_id,
+ 'id2' => $row['pid'],
+ );
+
+ $ext = explode( '.', $row['filename'] );
+ $ext = array_pop( $ext );
+
+ $info = array(
+ 'attach_id' => $row['aid'],
+ 'attach_file' => $row['filename'],
+ 'attach_date' => $row['dateuploaded'],
+ 'attach_member_id' => $row['uid'],
+ 'attach_hits' => $row['downloads'],
+ 'attach_ext' => $ext,
+ 'attach_filesize' => $row['filesize'],
+ );
+
+ $libraryClass->convertAttachment( $info, $map, rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $row['attachname'] );
+
+ $libraryClass->setLastKeyValue( $row['aid'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showthread.php' ) !== FALSE )
+ {
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ $oldId = \IPS\Request::i()->tid;
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'forumdisplay.php' ) !== FALSE )
+ {
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ $oldId = \IPS\Request::i()->fid;
+ }
+
+ if( isset( $class ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( $oldId, $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Phpbb.php b/applications/convert/sources/Software/Forums/Phpbb.php
new file mode 100644
index 00000000..6053d7f8
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Phpbb.php
@@ -0,0 +1,549 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Phpbb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "phpBB 3.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "phpbb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'forums',
+ 'where' => NULL,
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'topics',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'posts',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'attachments',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertForumsForums',
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertForumsForums':
+ $return['convertForumsForums'] = array();
+ $forums = $this->db->select( '*', 'forums' );
+ foreach( $forums AS $forum )
+ {
+ if ( $forum['forum_password'] )
+ {
+ \IPS\Member::loggedIn()->language()->words["forum_password_{$forum['forum_id']}"] = \IPS\Member::loggedIn()->language()->addToStack( 'forum_password', FALSE, array( 'sprintf' => array( $forum['forum_name'] ) ) );
+ \IPS\Member::loggedIn()->language()->words["forum_password_{$forum['forum_id']}_desc"] = \IPS\Member::loggedIn()->language()->addToStack( 'forum_password_desc' );
+
+ $return['convertForumsForums']["forum_password_{$forum['forum_id']}"] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(),
+ 'field_hint' => NULL,
+ );
+ }
+ }
+ break;
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_phpbb_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'phpbb' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Phpbb::fixPostData( $post );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'forum_id' );
+
+ foreach( $this->fetch( 'forums', 'forum_id' ) AS $row )
+ {
+ $info = array(
+ 'id' => $row['forum_id'],
+ 'name' => $row['forum_name'],
+ 'description' => $row['forum_desc'],
+ 'topics' => isset( $row['forum_topics_approved'] ) ? $row['forum_topics_approved'] : $row['forum_topics'],
+ 'posts' => isset( $row['forum_posts_approved'] ) ? $row['forum_posts_approved'] : $row['forum_posts'],
+ 'last_post' => $row['forum_last_post_time'],
+ 'last_poster_id' => $row['forum_last_poster_id'],
+ 'last_poster_name' => $row['forum_last_poster_name'],
+ 'parent_id' => ( $row['parent_id'] != 0 ) ? $row['parent_id'] : -1,
+ 'conv_parent' => ( $row['parent_id'] != 0 ) ? $row['parent_id'] : -1,
+ 'position' => $row['left_id'],
+ 'password' => ( isset( $this->app->_session['more_info']['convertForumsForums']["forum_password_{$row['forum_id']}"] ) ) ? $this->app->_session['more_info']['convertForumsForums']["forum_password_{$row['forum_id']}"] : NULL,
+ 'last_title' => $row['forum_last_post_subject'],
+ 'queued_topics' => isset( $row['forum_topics_unapproved'] ) ? $row['forum_topics_unapproved'] : 0,
+ 'queued_posts' => isset( $row['forum_posts_unapproved'] ) ? $row['forum_topics_unapproved'] : 0,
+ 'sub_can_post' => ( $row['forum_type'] == 1 ) ? TRUE : FALSE
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'forums_watch', array( "forum_id=?", $row['forum_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'forum',
+ 'follow_rel_id' => $row['forum_id'],
+ 'follow_rel_id_type' => 'forums_forums',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['forum_id'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'topic_id' );
+
+ foreach( $this->fetch( 'topics', 'topic_id' ) AS $row )
+ {
+ $poll = NULL;
+
+ if ( $row['poll_title'] )
+ {
+ $poll = array();
+
+ $choices = array();
+ $votes = array();
+ $index = 1;
+ $search = array(); # make sure we actually assign the vote correctly
+ foreach( $this->db->select( '*', 'poll_options', array( "topic_id=?", $row['topic_id'] ) ) AS $choice )
+ {
+ $choices[$index] = $choice['poll_option_text'];
+ $votes[$index] = $choice['poll_option_total'];
+ $search[$index] = $choice['poll_option_id'];
+ $index++;
+ }
+
+ $poll['poll_data'] = array(
+ 'pid' => $row['topic_id'],
+ 'choices' => array( 1 => array(
+ 'question' => $row['poll_title'],
+ 'multi' => ( $row['poll_max_options'] > 1 ) ? 1 : 0,
+ 'choice' => $choices,
+ 'votes' => $votes,
+ ) ),
+ 'poll_question' => $row['poll_title'],
+ 'start_date' => $row['poll_start'],
+ 'starter_id' => $row['topic_poster'],
+ 'votes' => array_sum( $votes ),
+ 'poll_view_voters' => 0,
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'poll_votes', array( "topic_id=?", $row['topic_id'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[$vote['vote_user_id']] ) )
+ {
+ $ourVotes[$vote['vote_user_id']] = array( 'votes' => array() );
+ }
+
+ $ourVotes[$vote['vote_user_id']]['votes'][] = array_search( $vote['poll_option_id'], $search );
+ $ourVotes[$vote['vote_user_id']]['member_id'] = $vote['vote_user_id'];
+ }
+
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][$member_id] = array(
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+
+ if ( isset( $row['topic_visibility'] ) )
+ {
+ $visibility = $row['topic_visibility'];
+ }
+ else
+ {
+ $visibility = $row['topic_approved'];
+ }
+
+ /* Global Topics */
+ if ( !$row['forum_id'] )
+ {
+ try
+ {
+ $orphaned = $this->app->getLink( '__global__', 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Create a forum to store it in */
+ $libraryClass->convertForumsForum( array(
+ 'id' => '__global__',
+ 'name' => 'Global Topics',
+ ) );
+ }
+
+ $row['forum_id'] = '__global__';
+ }
+
+ $info = array(
+ 'tid' => $row['topic_id'],
+ 'title' => $row['topic_title'],
+ 'forum_id' => $row['forum_id'],
+ 'state' => ( $row['topic_status'] == 0 ) ? 'open' : 'closed',
+ 'posts' => isset( $row['topic_posts_approved'] ) ? $row['topic_posts_approved'] : $row['topic_replies'],
+ 'starter_id' => $row['topic_poster'],
+ 'start_date' => $row['topic_time'],
+ 'last_poster_id' => $row['topic_last_poster_id'],
+ 'last_post' => $row['topic_last_post_time'],
+ 'starter_name' => $row['topic_first_poster_name'],
+ 'last_poster_name' => $row['topic_last_poster_name'],
+ 'poll_state' => $poll,
+ 'last_vote' => $row['poll_last_vote'],
+ 'views' => $row['topic_views'],
+ 'approved' => ( $visibility <> 1 ) ? -1 : 1,
+ 'pinned' => ( $row['topic_type'] == 0 ) ? 0 : 1,
+ 'topic_hiddenposts' => ( isset( $row['topic_posts_unapproved'] ) AND isset( $row['topic_posts_softdeleted'] ) ) ? $row['topic_posts_unapproved'] + $row['topic_posts_softdeleted'] : 0,
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'topics_watch', array( "topic_id=?", $row['topic_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['topic_id'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['topic_id'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'post_id' );
+
+ foreach( $this->fetch( 'posts', 'post_id' ) AS $row )
+ {
+ if ( isset( $row['post_visibility'] ) )
+ {
+ $visibility = $row['post_visibility'];
+ }
+ else
+ {
+ $visibility = $row['post_approved'];
+ }
+
+ $info = array(
+ 'pid' => $row['post_id'],
+ 'topic_id' => $row['topic_id'],
+ 'post' => \IPS\convert\Software\Core\Phpbb::stripUid( $row['post_text'], $row['bbcode_uid'] ),
+ 'append_edit' => ( $row['post_edit_user'] ) ? 1 : 0,
+ 'edit_time' => $row['post_edit_time'],
+ 'author_id' => $row['poster_id'],
+ 'ip_address' => $row['poster_ip'],
+ 'post_date' => $row['post_time'],
+ 'queued' => ( $visibility == 1 ) ? 0 : -1,
+ 'pdelete_time' => isset( $row['post_delete_time'] ) ? $row['post_delete_time'] : NULL,
+ );
+
+ $libraryClass->convertForumsPost( $info );
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'warnings', array( "post_id=?", $row['post_id'] ) ) AS $warning )
+ {
+ try
+ {
+ $log = $this->db->select( '*', 'log', array( "log_id=?", $warning['log_id'] ) )->first();
+ $data = \unserialize( $log['log_data'] );
+ }
+ catch( \UnderflowException $e )
+ {
+ $log = array( 'user_id' => 0 );
+ $data = array( 0 => NULL );
+ }
+
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['warning_id'],
+ 'wl_member' => $warn['user_id'],
+ 'wl_moderator' => $log['user_id'],
+ 'wl_date' => $warn['warning_time'],
+ 'wl_points' => 1,
+ 'wl_note_member' => isset( $data[0] ) ? $data[0] : NULL,
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['post_id'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'attach_id' );
+
+ foreach( $this->fetch( 'attachments', 'attach_id' ) AS $row )
+ {
+ $map = array(
+ 'id1' => $row['topic_id'],
+ 'id2' => $row['post_msg_id'],
+ );
+
+ $info = array(
+ 'attach_id' => $row['attach_id'],
+ 'attach_file' => $row['real_filename'],
+ 'attach_date' => $row['filetime'],
+ 'attach_member_id' => $row['poster_id'],
+ 'attach_hits' => $row['download_count'],
+ 'attach_ext' => $row['extension'],
+ 'attach_filesize' => $row['filesize'],
+ );
+
+ $libraryClass->convertAttachment( $info, $map, rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $row['physical_filename'] );
+
+ $libraryClass->setLastKeyValue( $row['attach_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'viewtopic.php' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->p ) )
+ {
+ $class = '\IPS\forums\Topic\Post';
+ $types = array( 'posts', 'forums_posts' );
+ $oldId = \IPS\Request::i()->p;
+ }
+ else
+ {
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ $oldId = \IPS\Request::i()->tid;
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'viewforum.php' ) !== FALSE )
+ {
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ $oldId = \IPS\Request::i()->f;
+ }
+
+ if( isset( $class ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( $oldId, $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Phpmyforum.php b/applications/convert/sources/Software/Forums/Phpmyforum.php
new file mode 100644
index 00000000..1422e948
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Phpmyforum.php
@@ -0,0 +1,109 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 6 December 2016
+ * @note Only redirect scripts are supported right now
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Phpmyforum extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "PhpMyForum";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "phpmyforum";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return NULL
+ */
+ public static function canConvert()
+ {
+ return NULL;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'board.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->id, array( 'forums', 'forums_forums' ) );
+ $item = \IPS\forums\Forum::load( $data );
+
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'topic.php' ) !== FALSE )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->id, array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->id, array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Punbb.php b/applications/convert/sources/Software/Forums/Punbb.php
new file mode 100644
index 00000000..abed5219
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Punbb.php
@@ -0,0 +1,360 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Punbb extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "PunBB 1.x";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "punbb";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'forums',
+ 'where' => NULL,
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'topics',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'posts',
+ 'where' => NULL
+ ),
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'punbb' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Punbb::fixPostData( $post );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'forums', 'id' ) AS $row )
+ {
+ /* PunBB has separate concepts of categories versus forums - normally, we would do these in separate processes but that isn't really all that necessary */
+ try
+ {
+ $catId = $this->app->getLink( '1000' . $row['cat_id'], 'forums_forums' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ try
+ {
+ $category = $this->db->select( '*', 'categories', array( "id=?", $row['cat_id'] ) )->first();
+
+ $libraryClass->convertForumsForum( array(
+ 'id' => '1000' . $category['id'],
+ 'name' => $category['cat_name'],
+ 'parent_id' => -1,
+ 'position' => $category['disp_position']
+ ) );
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ /* They don't store the last poster ID. Makes me sad. */
+ $last_poster_id = 0;
+ try
+ {
+ /* Better hope they haven't changed their name */
+ $last_poster_id = $this->db->select( 'id', 'users', array( "username=?", $row['last_poster'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'id' => $row['id'],
+ 'name' => $row['forum_name'],
+ 'description' => $row['forum_desc'],
+ 'topics' => $row['num_topics'],
+ 'posts' => $row['num_posts'],
+ 'last_post' => $row['last_post'],
+ 'last_poster_id' => $last_poster_id,
+ 'last_poster_name' => $row['last_poster'],
+ 'parent_id' => ( $row['parent_forum_id'] == 0 ) ? '1000' . $row['cat_id'] : $row['parent_forum_id'],
+ 'position' => $row['disp_position'],
+ 'redirect_url' => $row['redirect_url'],
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'topics', 'id' ) AS $row )
+ {
+ /* sigh */
+ $last_poster_id = 0;
+ try
+ {
+ $last_poster_id = $this->db->select( 'id', 'users', array( "username=?", $row['last_poster'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ $starter_id = 0;
+ try
+ {
+ $starter_id = $this->db->select( 'id', 'users', array( "username=?", $row['poster'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ $moved_to = NULL;
+ if ( $row['moved_to'] )
+ {
+ try
+ {
+ $moved_to_forum = $this->db->select( 'forum_id', 'topics', array( "id=?", $row['moved_to'] ) )->first();
+ $moved_to = [ $row['moved_to'], $moved_to_forum ];
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $info = array(
+ 'tid' => $row['id'],
+ 'title' => $row['subject'],
+ 'forum_id' => $row['forum_id'],
+ 'state' => ( $row['closed'] ) ? 'closed' : 'open',
+ 'posts' => $row['num_replies'],
+ 'starter_id' => $starter_id,
+ 'start_date' => $row['posted'],
+ 'last_poster_id' => $last_poster_id,
+ 'last_post' => $row['last_post'],
+ 'starter_name' => $row['poster'],
+ 'last_poster_name' => $row['last_poster'],
+ 'pinned' => ( $row['sticky'] OR $row['announcement'] ) ? 1 : 0,
+ 'moved_to' => $moved_to,
+ 'moved_on' => ( !is_null( $moved_to ) ) ? $row['posted'] : 0,
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ foreach( $this->db->select( '*', 'subscriptions', array( "topic_id=?", $row['id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['id'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['id'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id' );
+
+ foreach( $this->fetch( 'posts', 'id' ) AS $row )
+ {
+ $info = array(
+ 'pid' => $row['id'],
+ 'topic_id' => $row['topic_id'],
+ 'post' => $row['message'],
+ 'append_edit' => ( $row['edited'] ) ? 1 : 0,
+ 'edit_time' => $row['edited'],
+ 'author_id' => $row['poster_id'], # I half expected them to not store this
+ 'author_name' => $row['poster'],
+ 'ip_address' => $row['poster_ip'],
+ 'post_date' => $row['posted'],
+ 'edit_name' => $row['edited_by'],
+ );
+
+ $libraryClass->convertForumsPost( $info );
+
+ $libraryClass->setLastKeyvalue( $row['id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'viewtopic.php' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->pid ) )
+ {
+ $class = '\IPS\forums\Topic\Post';
+ $types = array( 'posts', 'forums_posts' );
+ $oldId = \IPS\Request::i()->pid;
+ }
+ else
+ {
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ $oldId = \IPS\Request::i()->id;
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'viewforum.php' ) !== FALSE )
+ {
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ $oldId = \IPS\Request::i()->id;
+ }
+
+ if( isset( $class ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( $oldId, $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Smf.php b/applications/convert/sources/Software/Forums/Smf.php
new file mode 100644
index 00000000..6aa7c9b8
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Smf.php
@@ -0,0 +1,596 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Smf extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Simple Machines Forum";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "smf";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsBoards' => array(
+ 'table' => 'boards',
+ 'where' => NULL
+ ),
+ 'convertForumsForums' => array(
+ 'table' => 'forums',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertForumsBoards' ),
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'topics',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'messages',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'attachments',
+ 'where' => array( "id_msg<>?", 0 )
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'forums':
+ return $this->db->select( 'COUNT(*)', 'categories' )->first() + $this->db->select( 'COUNT(*)', 'boards' )->first();
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'smf' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_smf_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Smf::fixPostData( $post );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_cat' );
+
+ foreach( $this->fetch( 'categories', 'id_cat', NULL, "id_cat, CONCAT( 'c', id_cat ) AS id, name, cat_order AS position, 0 AS sub_can_post, -1 AS parent_id" ) AS $row )
+ {
+ $current = $row['id_cat'];
+ unset( $row['id_cat'] );
+ $libraryClass->convertForumsForum( $row );
+
+ $libraryClass->setLastKeyValue( $current );
+ }
+ }
+
+ /**
+ * Convert categories
+ *
+ * @return void
+ */
+ public function convertForumsBoards()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_board' );
+
+ foreach( $this->fetch( 'boards', 'id_board' ) AS $row )
+ {
+ $last_post_time = 0;
+ $last_poster = 0;
+ $last_title = NULL;
+ $last_poster_name = NULL;
+
+ try
+ {
+ $last_post = $this->db->select( '*', 'messages', array( "id_msg=?", $row['id_last_msg'] ) )->first();
+
+ $last_post_time = $last_post['poster_time'];
+ $last_poster = $last_post['id_member'];
+ $last_poster_name = $last_post['poster_name'];
+ $last_title = $last_post['subject']; # Yes, I know this is technically wrong, but it's better than just not showing anything
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'id' => $row['id_board'],
+ 'name' => $row['name'],
+ 'description' => $row['description'],
+ 'topics' => $row['num_topics'],
+ 'posts' => $row['num_posts'],
+ 'last_post' => $last_post_time,
+ 'last_poster_id' => $last_poster,
+ 'last_poster_name' => $last_poster_name,
+ 'parent_id' => ( $row['id_parent'] <> 0 ) ? $row['id_parent'] : 'c' . $row['id_cat'],
+ 'position' => $row['board_order'],
+ 'last_title' => $last_title,
+ 'redirect_url' => $row['redirect'],
+ 'queued_topics' => $row['unapproved_topics'],
+ 'queued_posts' => $row['unapproved_posts'],
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ $libraryClass->setLastKeyValue( $row['id_board'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_topic' );
+
+ foreach( $this->fetch( 'topics', 'id_topic' ) AS $row )
+ {
+ try
+ {
+ $firstPost = $this->db->select( '*', 'messages', array( "id_msg=?", $row['id_first_msg'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $row['id_topic'] );
+ continue;
+ }
+
+ $lastPost = NULL;
+ try
+ {
+ $lastPost = $this->db->select( '*', 'messages', array( "id_msg=?", $row['id_last_msg'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+
+ $poll = NULL;
+ if ( $row['id_poll'] )
+ {
+ try
+ {
+ $poll_data = $this->db->select( '*', 'polls', array( "id_poll=?", $row['id_poll'] ) )->first();
+
+ $options = $this->db->select( '*', 'poll_choices', array( "id_poll=?", $poll_data['id_poll'] ) );
+
+ $choices = array();
+ $votes = 0;
+ foreach( $options AS $option )
+ {
+ $choices[ $option['id_choice'] ] = $option['label'];
+ $votes += $option['votes'];
+ }
+
+ $member_votes = array();
+ foreach( $this->db->select( '*', 'log_polls', array( "id_poll=?", $poll_data['id_poll'] ) ) AS $vote )
+ {
+ $member_votes[$vote['id_member']] = array(
+ 'member_id' => $vote['id_member'],
+ 'member_choices' => array( $vote['id_choice'] )
+ );
+ }
+
+ $poll = array(
+ 'poll_data' => array(
+ 'pid' => $poll_data['id_poll'],
+ 'choices' => $choices,
+ 'poll_question' => $poll_data['question'],
+ 'start_date' => $firstPost['poster_time'],
+ 'starter_id' => $poll_data['id_member'],
+ ),
+ 'vote_data' => $member_votes
+ );
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $info = array(
+ 'tid' => $row['id_topic'],
+ 'title' => $firstPost['subject'],
+ 'forum_id' => $row['id_board'],
+ 'state' => ( $row['locked'] ) ? 'closed' : 'open',
+ 'posts' => $row['num_replies'],
+ 'starter_id' => $row['id_member_started'],
+ 'start_date' => $firstPost['poster_time'],
+ 'last_poster_id' => $row['id_member_updated'],
+ 'last_post' => ( $lastPost ) ? $lastPost['poster_time'] : 0,
+ 'starter_name' => $firstPost['poster_name'],
+ 'last_poster_name' => $lastPost['poster_name'],
+ 'poll_state' => $poll,
+ 'views' => $row['num_views'],
+ 'approved' => $row['approved'],
+ 'pinned' => $row['is_sticky'],
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'log_notify', array( "id_topic=?", $row['id_topic'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['id_topic'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['id_member'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => 'immediate',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['id_topic'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_msg' );
+
+ foreach( $this->fetch( 'messages', 'id_msg' ) AS $row )
+ {
+ $info = array(
+ 'pid' => $row['id_msg'],
+ 'topic_id' => $row['id_topic'],
+ 'post' => $row['body'],
+ 'edit_time' => $row['modified_time'],
+ 'author_id' => $row['id_member'],
+ 'author_name' => $row['poster_name'],
+ 'ip_address' => $row['poster_ip'],
+ 'post_date' => $row['poster_time'],
+ 'queued' => ( $row['approved'] ) ? 0 : -1,
+ 'edit_name' => $row['modified_name'],
+ );
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['id_msg'] );
+ }
+ }
+
+ /**
+ * @brief Cached attachment paths
+ */
+ protected static $paths = NULL;
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ if ( is_null( static::$paths ) )
+ {
+ try
+ {
+ static::$paths = @\unserialize( $this->db->select( 'value', 'settings', array( "variable=?", 'attachmentUploadDir' ) )->first() );
+
+ /* Unserialize failed. Set to an array so we don't try again */
+ if ( !is_array( static::$paths ) )
+ {
+ static::$paths = array();
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ static::$paths = array();
+ }
+ }
+
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'id_attach' );
+
+ foreach( $this->fetch( 'attachments', 'id_attach' ) AS $row )
+ {
+ try
+ {
+ if ( $row['id_msg'] AND $row['attachment_type'] < 3 )
+ {
+ $post = $this->db->select( 'id_topic, poster_time', 'messages', array( "id_msg=?", $row['id_msg'] ) )->first();
+ }
+ else
+ {
+ /* Is an avatar or thumbnail */
+ throw new \UnderflowException;
+ }
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $row['id_attach'] );
+ continue;
+ }
+
+ /* Map */
+ $map = array(
+ 'id1' => $post['id_topic'],
+ 'id2' => $row['id_msg'],
+ );
+
+ /* We need to figure out where it is */
+ if ( $row['file_hash'] )
+ {
+ $location = $row['id_attach'] . '_' . $row['file_hash'];
+ }
+ else
+ {
+ /* Clean filename per legacy SMF requirements */
+ $cleanName = strtr( $row['filename'], 'ŠŽšžŸÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ', 'SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy');
+ $cleanName = strtr( $cleanName, array( 'Þ' => 'TH', 'þ' => 'th', 'Ð' => 'DH', 'ð' => 'dh', 'ß' => 'ss', 'Œ' => 'OE', 'œ' => 'oe', 'Æ' => 'AE', 'æ' => 'ae', 'µ' => 'u' ) );
+ $cleanName = preg_replace( array( '/s/', '/[^w_.-]/' ), array( '_', '' ), $cleanName );
+ $location = $row['id_attach'] . '_' . str_replace( '.', '_', $cleanName ) . md5( $cleanName );
+ }
+ $location = str_replace( ' ', '_', $location );
+
+ /* Fix File Name */
+ // http://community.invisionpower.com/tracker/issue-21020-smf-attachments-are-crazy/
+ $row['filename'] = str_replace(' ', '_', $row['filename']);
+ $row['filename'] = str_replace('!', '', $row['filename']);
+ $row['filename'] = str_replace('(', '', $row['filename']);
+ $row['filename'] = str_replace(')', '', $row['filename']);
+ $row['filename'] = str_replace(',', '', $row['filename']);
+ $row['filename'] = str_replace('[', '', $row['filename']);
+ $row['filename'] = str_replace(']', '', $row['filename']);
+ $row['filename'] = str_replace('&', '', $row['filename']);
+ $row['filename'] = str_replace('\'', '', $row['filename']);
+ $row['filename'] = str_replace( chr(195) . chr(182) , 'A', $row['filename']);
+ $row['filename'] = str_replace( chr(195) . chr(164) , 'A', $row['filename']);
+
+ /* General Information */
+ $info = array(
+ 'attach_id' => $row['id_attach'],
+ 'attach_file' => $row['filename'],
+ 'attach_member_id' => $row['id_member'],
+ 'attach_hits' => $row['downloads'],
+ 'attach_date' => $post['poster_time'],
+ 'attach_ext' => $row['fileext'],
+ 'attach_filesize' => $row['size']
+ );
+
+ if ( $row['id_folder'] AND $row['id_folder'] != 1 )
+ {
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $location;
+ }
+ else
+ {
+ if ( isset( static::$paths[ $row['id_folder'] ] ) )
+ {
+ $path = rtrim( static::$paths[ $row['id_folder'] ], '/' ) . '/' . $location;
+ }
+ else
+ {
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $location;
+ }
+ }
+
+ $libraryClass->convertAttachment( $info, $map, $path );
+
+ $libraryClass->setLastKeyValue( $row['id_attach'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ if( isset( \IPS\Request::i()->topic ) )
+ {
+ if( mb_strpos( \IPS\Request::i()->topic, '.msg' ) !== FALSE )
+ {
+ $class = '\IPS\forums\Topic\Post';
+ $types = array( 'posts', 'forums_posts' );
+ $oldId = mb_substr( \IPS\Request::i()->topic, mb_strpos( \IPS\Request::i()->topic, '.msg' ) + 4 );
+ }
+ else
+ {
+ $pieces = explode( '.', \IPS\Request::i()->topic );
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ $oldId = $pieces[0];
+ }
+ }
+ elseif( isset( \IPS\Request::i()->board ) )
+ {
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ $oldId = \IPS\Request::i()->board;
+ }
+
+ if( isset( $class ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( $oldId, $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/UBBthreads.php b/applications/convert/sources/Software/Forums/UBBthreads.php
new file mode 100644
index 00000000..218bbf2d
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/UBBthreads.php
@@ -0,0 +1,536 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2015 Invision Power Services, Inc.
+ * @package IPS Social Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _UBBthreads extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "UBBthreads";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "UBBthreads";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'CATEGORIES',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertForumsForumsFollowers' )
+ ),
+ 'convertForumsForumsFollowers' => array(
+ 'table' => 'WATCH_LISTS',
+ 'where' => array( 'WATCH_TYPE=?', 'f' )
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'TOPICS',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertForumsTopicsRatings', 'convertForumsTopicsFollowers' )
+ ),
+ 'convertForumsTopicsRatings' => array(
+ 'table' => 'RATINGS',
+ 'where' => array( 'RATING_TYPE=?', 't' )
+ ),
+ 'convertForumsTopicsFollowers' => array(
+ 'table' => 'WATCH_LISTS',
+ 'where' => array( 'WATCH_TYPE=?', 't' )
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'POSTS',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'FILES',
+ 'where' => array( 'POST_ID>?', 0 )
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch ( $table )
+ {
+ case 'CATEGORIES':
+ return parent::countRows( 'CATEGORIES' ) + parent::countRows( 'FORUMS' );
+ }
+
+ return parent::countRows( $table, $where, $recache );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'UBBthreads' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_ubb_attach_path'),
+ ),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\UBBthreads::fixPostData( $post );
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ /**
+ * Categories in UBB are abstracted. So, before we actually convert any real "forums", we will want to convert
+ * the categories they are contained in.
+ */
+ foreach ( $this->db->select( '*', 'CATEGORIES' ) as $row )
+ {
+ $libraryClass->convertForumsForum( array(
+ 'id' => 10000 + $row['CATEGORY_ID'],
+ 'name' => $row['CATEGORY_TITLE'],
+ 'description' => $row['CATEGORY_DESCRIPTION'],
+ 'position' => $row['CATEGORY_SORT_ORDER'],
+ 'sub_can_post' => 0,
+ 'parent_id' => -1
+ ) );
+ }
+
+ /* Here is where we actually convert the real forums */
+ foreach( $this->db->select( '*', 'FORUMS' ) AS $row )
+ {
+ $libraryClass->convertForumsForum( array(
+ 'id' => $row['FORUM_ID'],
+ 'name' => $row['FORUM_TITLE'],
+ 'description' => $row['FORUM_DESCRIPTION'],
+ 'topics' => $row['FORUM_TOPICS'],
+ 'posts' => $row['FORUM_POSTS'],
+ 'last_post' => \IPS\DateTime::create()->setTimestamp( $row['FORUM_LAST_POST_TIME'] ),
+ 'last_poster_id' => $row['FORUM_LAST_POSTER_ID'],
+ 'last_poster_name' => $row['FORUM_LAST_POSTER_NAME'],
+ 'parent_id' => $row['FORUM_PARENT_ID'] ?: ( 10000 + $row['CATEGORY_ID'] ),
+ 'position' => $row['FORUM_SORT_ORDER'],
+ 'last_title' => $row['FORUM_LAST_POST_SUBJECT'],
+ 'allow_poll' => $row['FORUM_ALLOW_POLLS']
+ ) );
+ }
+
+ throw new \IPS\convert\Software\Exception;
+ }
+
+ /**
+ * Convert forum followers
+ *
+ * @return void
+ */
+ public function convertForumsForumsFollowers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach ( $this->fetch( 'WATCH_LISTS', 'WATCH_ID', array( 'WATCH_TYPE=?', 'f' ), 'USER_ID ASC, WATCH_ID ASC' ) as $row )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'forum',
+ 'follow_rel_id' => $row['WATCH_ID'],
+ 'follow_rel_id_type' => 'forums_forums',
+ 'follow_member_id' => $row['USER_ID'],
+ 'follow_notify_freq' => $row['WATCH_NOTIFY_IMMEDIATE'] ? 'immediate' : 'none',
+ ) );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'TOPIC_ID' );
+
+ foreach ( $this->fetch( 'TOPICS', 'TOPIC_ID' ) as $row )
+ {
+ /* Poll */
+ $poll = NULL;
+ if ( $row['TOPIC_HAS_POLL'] > 0 )
+ {
+ try
+ {
+ $pollData = $this->db->select( '*', 'POLL_DATA', array( "POST_ID=?", $row['POST_ID'] ) )->first();
+
+ $choices = array();
+ $index = 1;
+ foreach( $this->db->select( '*', 'POLL_OPTIONS', array( 'POLL_ID=?', $pollData['POLL_ID'] ), 'OPTION_ID ASC' ) AS $choice )
+ {
+ $choices[ $index ] = strip_tags( $choice['CHOICE_BODY'] ); // Unlike forum titles, I don't believe we allow HTML in poll choices
+ $index++;
+ }
+
+ /* Reset Index */
+ $index = 1;
+ $votes = array();
+ $rawVotes = iterator_to_array( $this->db->select( '*', 'POLL_VOTES', array( 'POLL_ID=?', $pollData['POLL_ID'] ) ) );
+ foreach( $rawVotes AS $vote )
+ {
+ $votes[ $index ] = $vote['OPTION_ID'];
+ $index++;
+ }
+
+ $poll = array();
+ $poll['poll_data'] = array(
+ 'pid' => $pollData['POLL_ID'],
+ 'choices' => array( 1 => array(
+ 'question' => strip_tags( $pollData['POLL_BODY'] ),
+ 'multi' => ( $pollData['POLL_TYPE'] != 'one' ),
+ 'choice' => $choices,
+ 'votes' => $votes
+ ) ),
+ 'poll_question' => strip_tags( $pollData['POLL_BODY'] ),
+ 'start_date' => \IPS\DateTime::create()->setTimestamp( $row['POLL_START_TIME'] ),
+ 'starter_id' => $row['USER_ID'],
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $rawVotes AS $vote )
+ {
+ /* "Votes need a member account", apparently, so we will probably lose guest votes */
+ if ( !isset( $ourVotes[ $vote['VOTES_USER_ID_IP'] ] ) )
+ {
+ $ourVotes[ $vote['VOTES_USER_ID_IP'] ] = array( 'votes' => array() );
+ }
+
+ $ourVotes[ $vote['uid'] ]['votes'][] = $vote['OPTION_ID'];
+ $ourVotes[ $vote['uid'] ]['member_id'] = $vote['VOTES_USER_ID_IP'];
+ }
+
+ /* Now we need to re-wrap it all for storage */
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][ $member_id ] = array(
+ 'vote_date' => $vote['vote_date'],
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+ catch( \UnderflowException $e ) {} # if the poll is missing, don't bother
+ }
+
+ $libraryClass->convertForumsTopic( array(
+ 'tid' => $row['TOPIC_ID'],
+ 'title' => mb_substr( $row['TOPIC_SUBJECT'], 0, 250 ),
+ 'forum_id' => $row['FORUM_ID'],
+ 'state' => ( $row['TOPIC_STATUS'] == 'C' ) ? 'closed' : 'open',
+ 'posts' => $row['TOPIC_TOTAL_REPLIES'],
+ 'starter_id' => $row['USER_ID'],
+ 'start_date' => \IPS\DateTime::create()->setTimestamp( $row['TOPIC_CREATED_TIME'] ),
+ 'last_poster_id' => $row['TOPIC_LAST_POSTER_ID'],
+ 'last_post' => \IPS\DateTime::create()->setTimestamp( $row['TOPIC_LAST_REPLY_TIME'] ),
+ 'last_poster_name' => $row['TOPIC_LAST_POSTER_NAME'],
+ 'views' => $row['TOPIC_VIEWS'],
+ 'approved' => $row['TOPIC_IS_APPROVED'],
+ 'pinned' => $row['TOPIC_IS_STICKY'],
+ 'poll_state' => $poll
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['TOPIC_ID'] );
+ }
+ }
+
+ /**
+ * Convert topic ratings
+ *
+ * @return void
+ */
+ public function convertForumsTopicsRatings()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach ( $this->db->select( '*', 'RATINGS', array( 'RATING_TYPE=?', 't' ) ) as $row )
+ {
+ $libraryClass->convertRating( array(
+ 'class' => 'IPS\\forums\\Topic',
+ 'item_link' => 'forums_topics',
+ 'item_id' => $row['RATING_TARGET'],
+ 'rating' => $row['RATING_VALUE'],
+ 'member' => $row['RATING_RATER']
+ ) );
+ }
+ }
+
+ /**
+ * Convert topic followers
+ *
+ * @return void
+ */
+ public function convertForumsTopicsFollowers()
+ {
+ $libraryClass = $this->getLibrary();
+
+ foreach ( $this->fetch( 'WATCH_LISTS', 'WATCH_ID', array( 'WATCH_TYPE=?', 't' ), 'USER_ID ASC, WATCH_ID ASC' ) as $row )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['WATCH_ID'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $row['USER_ID'],
+ 'follow_notify_freq' => $row['WATCH_NOTIFY_IMMEDIATE'] ? 'immediate' : 'none',
+ ) );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'POST_ID' );
+
+ foreach ( $this->fetch( 'POSTS', 'POST_ID' ) as $row )
+ {
+ $libraryClass->convertForumsPost( array(
+ 'pid' => $row['POST_ID'],
+ 'topic_id' => $row['TOPIC_ID'],
+ 'post' => $row['POST_DEFAULT_BODY'],
+ 'append_edit' => ( (int) $row['POST_LAST_EDITED_TIME'] > 0 ),
+ 'edit_time' => $row['POST_LAST_EDITED_TIME'],
+ 'edit_name' => $row['POST_LAST_EDITED_BY'],
+ 'author_id' => $row['USER_ID'],
+ 'author_name' => $row['POST_POSTER_NAME'],
+ 'ip_address' => $row['POST_POSTER_IP'],
+ 'post_date' => \IPS\DateTime::create()->setTimestamp( $row['POST_POSTED_TIME'] ),
+ 'queued' => ( ! $row['POST_IS_APPROVED'] ),
+ 'new_topic' => $row['POST_IS_TOPIC'],
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['POST_ID'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'FILE_ID' );
+
+ /* Only load files with a post id */
+ foreach( $this->fetch( 'FILES', 'FILE_ID', array( 'POST_ID>?', 0 ) ) AS $row )
+ {
+ $map = array();
+
+ /* Try to load the topic ID */
+ try
+ {
+ $topicId = $this->db->select( 'TOPIC_ID', 'POSTS', array( "POST_ID=?", $row['POST_ID'] ) )->first();
+
+ $map['id1'] = $topicId;
+ $map['id2'] = $row['POST_ID'];
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'attach_id' => $row['FILE_ID'],
+ 'attach_file' => $row['FILE_NAME'],
+ 'attach_date' => \IPS\DateTime::create()->setTimestamp( $row['FILE_ADD_TIME'] ),
+ 'attach_member_id' => $row['USER_ID'],
+ 'attach_hits' => $row['FILE_DOWNLOADS'],
+ 'attach_ext' => pathinfo( $row['FILE_NAME'], \PATHINFO_EXTENSION ),
+ 'attach_filesize' => $row['FILE_SIZE'], // Note: Apparently not always readily available?
+ );
+
+ $libraryClass->convertAttachment( $info, $map, rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $row['FILE_NAME'] );
+ $libraryClass->setLastKeyValue( $row['FILE_ID'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ /* Make sure it's a UBBThreads URL */
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'ubbthreads.php' ) === FALSE )
+ {
+ return NULL;
+ }
+
+ if( preg_match( '#/ubbthreads.php/topics/([0-9]+)($|/)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ elseif( preg_match( '#/ubbthreads.php/forums/([0-9]+)($|/)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'forums', 'forums_forums' ) );
+ $item = \IPS\forums\Forum::load( $data );
+
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Vanilla.php b/applications/convert/sources/Software/Forums/Vanilla.php
new file mode 100644
index 00000000..88017be8
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Vanilla.php
@@ -0,0 +1,522 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2015 Invision Power Services, Inc.
+ * @package IPS Social Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ * @version
+ */
+
+namespace IPS\convert\Software\Forums;
+use \IPS\convert\Software\Core\Vanilla as VanillaCore;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vanilla extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Vanilla 2";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vanilla";
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vanilla' ) );
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'Category',
+ 'where' => NULL
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'Discussion',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'Comment',
+ 'where' => NULL,
+ 'extra_steps' => array( 'convertForumsPosts2' )
+ ),
+ 'convertForumsPosts2' => array(
+ 'table' => 'Comment',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'Media',
+ 'where' => array( 'ForeignTable=? OR ForeignTable=?', 'discussion', 'comment' )
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'Comment':
+ $count = 0;
+ $count += $this->db->select( 'COUNT(*)', 'Discussion' )->first();
+ $count += $this->db->select( 'COUNT(*)', 'Comment' )->first();
+ return $count;
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Can we convert passwords from this software.
+ *
+ * @return boolean
+ */
+ public static function loginEnabled()
+ {
+ return TRUE;
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertForums',
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+
+ switch( $method )
+ {
+ case 'convertForums':
+ $return['convertForums'] = array();
+
+ /* Find out where the photos live */
+ \IPS\Member::loggedIn()->language()->words['attach_location_desc'] = \IPS\Member::loggedIn()->language()->addToStack( 'attach_location' );
+ $return['convertForums']['attach_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_vanilla_photopath'),
+ );
+ break;
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_vanilla_photopath'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ ),
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'forums_topics', 'class' => 'IPS\forums\Topic' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics", "f_topic_tags_recount" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ /**
+ * Vanilla actually only supports plaintext posts out of the box, but there is a popular quotes plugin I think
+ * we should accommodate for.
+ */
+ $quoted = preg_replace_callback( '#(.+) #', function( $match )
+ {
+ $match[1] = str_replace( '"', '', $match[1] );
+ return "[quote name=\"{$match[1]}\"]{$match[2]}[/quote]";
+ }, $post );
+
+ return $quoted ?: $post;
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'MediaID' );
+
+ foreach( $this->fetch( 'Media', 'MediaID', array( 'ForeignTable=? OR ForeignTable=?', 'discussion', 'comment' ) ) AS $row )
+ {
+ if( $row['ForeignTable'] == 'discussion' )
+ {
+ $map = array(
+ 'id1' => 'fp-' . $row['ForeignID'],
+ 'id2' => $row['ForeignID'],
+ );
+ }
+ else
+ {
+ try
+ {
+ $discussionId = $this->db->select( 'DiscussionID', 'Comment', array( 'CommentID=?', $row['ForeignID'] ) )->first();
+ }
+ catch( \UnderflowException $ex )
+ {
+ $libraryClass->setLastKeyValue( $row['MediaID'] );
+ }
+
+ $map = array(
+ 'id1' => $discussionId,
+ 'id2' => $row['ForeignID'],
+ );
+ }
+
+ /* File extension */
+ $ext = explode( '.', $row['Path'] );
+ $ext = array_pop( $ext );
+
+ $info = array(
+ 'attach_id' => $row['MediaID'],
+ 'attach_file' => $row['Name'],
+ 'attach_date' => VanillaCore::mysqlToDateTime( $row['DateInserted'] ),
+ 'attach_member_id' => $row['InsertUserID'],
+ 'attach_hits' => 0,
+ 'attach_ext' => $ext,
+ 'attach_filesize' => $row['Size'],
+ );
+
+ $libraryClass->convertAttachment( $info, $map, rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . trim( $row['Path'], '/' ) );
+ $libraryClass->setLastKeyValue( $row['MediaID'] );
+ }
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'c.CategoryID' );
+
+ $uploadsPath = $this->app->_session['more_info']['convertAttachments']['attach_location'];
+
+ $forums = $this->fetch( array( 'Category', 'c' ), 'CategoryID', array( 'c.CategoryID<>?', -1 ),
+ 'c.*, lcu.UserID as LastCommentUserID, lcu.Name as LastCommentUserName, ld.Name as LastDiscussionName'
+ );
+ $forums->join( array( 'User', 'lcu' ), 'c.LastCommentID=lcu.UserID' );
+ $forums->join( array( 'Discussion', 'ld' ), 'c.LastDiscussionID=ld.DiscussionID' );
+
+ foreach( $forums AS $row )
+ {
+ $icon = $row['Icon'] ? VanillaCore::parseMediaLocation( $row['Icon'], $uploadsPath ) : NULL;
+ $info = [
+ 'id' => $row['CategoryID'],
+ 'name' => $row['Name'],
+ 'description' => $row['Description'],
+ 'topics' => $row['CountDiscussions'],
+ 'posts' => $row['CountComments'],
+ 'last_post' => VanillaCore::mysqlToDateTime( $row['LastDateInserted'] ),
+ 'last_poster_id' => $row['LastCommentID'],
+ 'last_poster_name' => $row['LastCommentUserName'],
+ 'parent_id' => ( (int) $row['ParentCategoryID'] > 0 ) ? $row['ParentCategoryID'] : NULL,
+ 'position' => $row['Sort'],
+ 'last_title' => $row['LastDiscussionName'],
+ 'icon' => $icon,
+ 'sub_can_post' => $row['AllowDiscussions'] ?: 0
+ ];
+
+ $libraryClass->convertForumsForum( $info, NULL, $icon );
+ $libraryClass->setLastKeyValue( $row['CategoryID'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'd.DiscussionID' );
+
+ $discussions = $this->fetch( array( 'Discussion', 'd' ), 'DiscussionID', NULL,
+ 'd.*, u.Name as UserName, lcu.UserID as LastCommentUserID, lcu.Name as LastCommentUserName'
+ );
+ $discussions->join( array( 'User', 'u' ), 'd.InsertUserID=u.UserID' );
+ $discussions->join( array( 'User', 'lcu' ), 'd.LastCommentUserID=lcu.UserID' );
+
+ foreach( $discussions AS $row )
+ {
+ /* If last post info is empty, fetch it */
+ if( $row['DateLastComment'] === NULL )
+ {
+ $data = $this->db->select( 'Comment.InsertUserID, Comment.DateInserted, User.Name', 'Comment', array( 'DiscussionID=?', $row['DiscussionID'] ), 'CommentID DESC', array( 0, 1 ) )
+ ->join( 'User', 'Comment.InsertUserID=User.UserID' )
+ ->first();
+
+ $row['DateLastComment'] = $data['DateInserted'];
+ $row['LastCommentUserId'] = $data['InsertUserId'];
+ $row['LastCommentUserName'] = $data['Name'];
+ }
+
+ $info = array(
+ 'tid' => $row['DiscussionID'],
+ 'title' => $row['Name'],
+ 'forum_id' => $row['CategoryID'],
+ 'state' => ( $row['Closed'] == 0 ) ? 'open' : 'closed',
+ 'posts' => $row['CountComments'],
+ 'starter_id' => $row['InsertUserID'],
+ 'start_date' => VanillaCore::mysqlToDateTime( $row['DateInserted'] ),
+ 'last_poster_id' => $row['LastCommentUserId'],
+ 'last_post' => VanillaCore::mysqlToDateTime( $row['DateLastComment'] ),
+ 'starter_name' => $row['UserName'],
+ 'last_poster_name' => $row['LastCommentUserName'],
+ 'views' => $row['CountViews'],
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ /* Tags */
+ if( !empty( $row['Tags'] ) )
+ {
+ $tags = explode( ',', $row['Tags'] );
+
+ if ( count( $tags ) )
+ {
+ foreach( $tags AS $tag )
+ {
+ $toConvert = explode( ' ', $tag );
+ foreach( $toConvert as $spacedTag )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $row['CategoryID'],
+ 'tag_meta_id' => $row['DiscussionID'],
+ 'tag_text' => $spacedTag,
+ 'tag_member_id' => $row['InsertUserID'],
+ 'tag_prefix' => 0,
+ ) );
+ }
+ }
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $row['DiscussionID'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'DiscussionID' );
+
+ foreach( $this->fetch( 'Discussion', 'DiscussionID' ) AS $row )
+ {
+ $editName = NULL;
+
+ if( $row['UpdateUserID'] )
+ {
+ try
+ {
+ $editName = $this->db->select( 'Name', 'User', array( 'UserID=?', $row['UpdateUserID'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ // First post
+ $info = array(
+ 'pid' => 'fp-' . $row['DiscussionID'],
+ 'topic_id' => $row['DiscussionID'],
+ 'post' => $row['Body'],
+ 'new_topic' => 1,
+ 'edit_time' => ( $editName === NULL ) ? NULL : VanillaCore::mysqlToDateTime( $row['DateUpdated'] ),
+ 'edit_name' => $editName,
+ 'author_id' => $row['InsertUserID'],
+ 'ip_address' => $row['InsertIPAddress'],
+ 'post_date' => VanillaCore::mysqlToDateTime( $row['DateInserted'] ),
+ );
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['DiscussionID'] );
+ }
+ }
+
+ /**
+ * Convert other posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts2()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'CommentID' );
+
+ foreach( $this->fetch( 'Comment', 'CommentID' ) AS $row )
+ {
+ $editName = NULL;
+
+ if( $row['UpdateUserID'] )
+ {
+ try
+ {
+ $editName = $this->db->select( 'Name', 'User', array( 'UserID=?', $row['UpdateUserID'] ) )->first();
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $info = [
+ 'pid' => $row['CommentID'],
+ 'topic_id' => $row['DiscussionID'],
+ 'post' => $row['Body'],
+ 'edit_time' => ( $editName === NULL ) ? NULL : VanillaCore::mysqlToDateTime( $row['DateUpdated'] ),
+ 'edit_name' => $editName,
+ 'author_id' => $row['InsertUserID'],
+ 'ip_address' => $row['InsertIPAddress'],
+ 'post_date' => VanillaCore::mysqlToDateTime( $row['DateInserted'] ),
+ ];
+
+ $libraryClass->convertForumsPost( $info );
+ $libraryClass->setLastKeyValue( $row['CommentID'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ * @note Forums and profiles don't use an ID in the URL. While we may be able to somehow cross reference this with our SEO slug, it wouldn't be reliable.
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/discussion/([0-9]+)/#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Vbulletin.php b/applications/convert/sources/Software/Forums/Vbulletin.php
new file mode 100644
index 00000000..66c6c93e
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Vbulletin.php
@@ -0,0 +1,999 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * @brief vBulletin 4 Stores all attachments under one table - this will store the content type for the forums app.
+ */
+ protected static $postContentType = NULL;
+
+ /**
+ * @brief The schematic for vB3 and vB4 is similar enough that we can make specific concessions in a single converter for either version.
+ */
+ protected static $isLegacy = NULL;
+
+ /**
+ * @brief Flag to indicate the post data has been fixed during conversion, and we only need to use Legacy Parser
+ */
+ public static $contentFixed = TRUE;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @param bool Establish a DB connection
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app, $needDB=TRUE )
+ {
+ $return = parent::__construct( $app, $needDB );
+
+ if ( $needDB )
+ {
+ try
+ {
+ /* Is this vB3 or vB4? */
+ if ( static::$isLegacy === NULL )
+ {
+ $version = $this->db->select( 'value', 'setting', array( "varname=?", 'templateversion' ) )->first();
+
+ if ( mb_substr( $version, 0, 1 ) == '3' )
+ {
+ static::$isLegacy = TRUE;
+ }
+ else
+ {
+ static::$isLegacy = FALSE;
+ }
+ }
+
+
+ /* If this is vB4, what is the content type ID for posts? */
+ if ( static::$postContentType === NULL AND ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) )
+ {
+ static::$postContentType = $this->db->select( 'contenttypeid', 'contenttype', array( "class=?", 'Post' ) )->first();
+ }
+ }
+ catch( \Exception $e ) {}
+ }
+
+ return $return;
+ }
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin Forums (3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'forum',
+ 'where' => NULL,
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'thread',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'post',
+ 'where' => NULL
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'attachment',
+ 'where' => ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) ? array( "contenttypeid=?", static::$postContentType ) : NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'forums_topics', 'class' => 'IPS\forums\Topic' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics", "f_topic_tags_recount" );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin::fixPostData( $post );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array( 'convertAttachments' );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'file_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'forumid' );
+
+ foreach( $this->fetch( 'forum', 'forumid' ) AS $forum )
+ {
+ $self = $this;
+ $checkpermission = function( $name, $perm ) use ( $forum, $self )
+ {
+ $key = $name;
+ if ( $name == 'forumoptions' )
+ {
+ $key = 'options';
+ }
+
+ if ( $forum[$key] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+ };
+
+ $info = array(
+ 'id' => $forum['forumid'],
+ 'name' => $forum['title'],
+ 'description' => $forum['description'],
+ 'topics' => $forum['threadcount'],
+ 'posts' => $forum['replycount'],
+ 'last_post' => $forum['lastpost'],
+ 'last_poster_id' => ( static::$isLegacy === FALSE or is_null( static::$isLegacy ) ) ? $forum['lastposterid'] : 0,
+ 'last_poster_name' => $forum['lastposter'],
+ 'parent_id' => $forum['parentid'],
+ 'position' => $forum['displayorder'],
+ 'password' => $forum['password'] ?: NULL,
+ 'last_title' => $forum['lastthread'],
+ 'preview_posts' => $checkpermission( 'forumoptions', 'moderatenewpost' ),
+ 'inc_postcount' => $checkpermission( 'forumoptions', 'countposts' ),
+ 'redirect_url' => $forum['link'],
+ 'sub_can_post' => $checkpermission( 'forumoptions', 'cancontainthreads' ),
+ 'forum_allow_rating' => $checkpermission( 'forumoptions', 'allowratings' ),
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ /* Follows for this forum */
+ foreach( $this->db->select( '*', 'subscribeforum', array( "forumid=?", $forum['forumid'] ) ) AS $follow )
+ {
+ $frequency = 'none';
+
+ switch( $follow['emailupdate'] )
+ {
+ case 1:
+ $frequency = 'immediate';
+ break;
+
+ case 2:
+ $frequency = 'daily';
+ break;
+
+ case 3:
+ $frequency = 'weekly';
+ break;
+ }
+
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'forum',
+ 'follow_rel_id' => $forum['forumid'],
+ 'follow_rel_id_type' => 'forums_forums',
+ 'follow_member_id' => $follow['userid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => $frequency,
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $forum['forumid'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'threadid' );
+
+ foreach( $this->fetch( 'thread', 'threadid' ) AS $topic )
+ {
+ /* Pesky Polls */
+ $poll = NULL;
+ $lastVote = 0;
+ if ( $topic['pollid'] > 0 )
+ {
+ try
+ {
+ $pollData = $this->db->select( '*', 'poll', array( "pollid=?", $topic['pollid'] ) )->first();
+
+ $lastVote = $pollData['lastvote'];
+
+ $choices = array();
+ $index = 1;
+ foreach( explode( '|||', $pollData['options'] ) AS $choice )
+ {
+ $choices[$index] = trim( $choice );
+ $index++;
+ }
+
+ /* Reset Index */
+ $index = 1;
+ $votes = array();
+ $numvotes = 0;
+ foreach( explode( '|||', $pollData['votes'] ) AS $vote )
+ {
+ $votes[$index] = $vote;
+ $index++;
+
+ $numvotes += $vote;
+ }
+
+ $poll = array();
+ $poll['poll_data'] = array(
+ 'pid' => $pollData['pollid'],
+ 'choices' => array( 1 => array(
+ 'question' => $pollData['question'],
+ 'multi' => $pollData['multiple'],
+ 'choice' => $choices,
+ 'votes' => $votes
+ ) ),
+ 'poll_question' => $pollData['question'],
+ 'start_date' => $pollData['dateline'],
+ 'starter_id' => $topic['postuserid'],
+ 'votes' => $numvotes,
+ 'poll_view_voters' => $pollData['public']
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'pollvote', array( "pollid=?", $pollData['pollid'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[$vote['userid']] ) )
+ {
+ /* Create our structure - vB stores each individual vote as a different row whereas we combine them per user */
+ $ourVotes[$vote['userid']] = array( 'votes' => array() );
+ }
+
+ $ourVotes[$vote['userid']]['votes'][] = $vote['voteoption'];
+
+ /* These don't matter - just use the latest one */
+ $ourVotes[$vote['userid']]['vid'] = $vote['pollvoteid'];
+ $ourVotes[$vote['userid']]['vote_date'] = $vote['votedate'];
+ $ourVotes[$vote['userid']]['member_id'] = $vote['userid'];
+ }
+
+ /* Now we need to re-wrap it all for storage */
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][$member_id] = array(
+ 'vid' => $vote['vid'],
+ 'vote_date' => $vote['vote_date'],
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+ catch( \UnderflowException $e ) {} # if the poll is missing, don't bother
+ }
+
+ $info = array(
+ 'tid' => $topic['threadid'],
+ 'title' => $topic['title'],
+ 'forum_id' => $topic['forumid'],
+ 'state' => $topic['open'] ? 'open' : 'closed',
+ 'posts' => $topic['replycount'],
+ 'starter_id' => $topic['postuserid'],
+ 'start_date' => $topic['dateline'],
+ 'last_poster_id' => ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) ? $topic['lastposterid'] : NULL,
+ 'last_post' => $topic['lastpost'],
+ 'starter_name' => $topic['postusername'],
+ 'last_poster_name' => $topic['lastposter'],
+ 'poll_state' => $poll,
+ 'last_vote' => $lastVote,
+ 'views' => $topic['views'],
+ 'approved' => $topic['visible'],
+ 'pinned' => $topic['sticky'],
+ 'topic_open_time' => $topic['dateline'],
+ 'topic_hiddenposts' => $topic['hiddencount'] + $topic['deletedcount']
+ );
+
+ unset( $poll );
+
+ $topic_id = $libraryClass->convertForumsTopic( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'subscribethread', array( "threadid=?", $topic['threadid'] ) ) AS $follow )
+ {
+ $frequency = 'none';
+ switch( $follow['emailupdate'] )
+ {
+ case 1:
+ $frequency = 'immediate';
+ break;
+
+ case 2:
+ $frequency = 'daily';
+ break;
+
+ case 3:
+ $frequency = 'weekly';
+ break;
+ }
+
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $topic['threadid'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['userid'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => $frequency,
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ /* Ratings */
+ foreach( $this->db->select( '*', 'threadrate', array( "threadid=?", $topic['threadid'] ) ) AS $rating )
+ {
+ $libraryClass->convertRating( array(
+ 'id' => $rating['threadrateid'],
+ 'class' => 'IPS\\forums\\Topic',
+ 'item_link' => 'forums_topics',
+ 'item_id' => $rating['threadid'],
+ 'ip' => $rating['ipaddress'],
+ 'rating' => $rating['vote'],
+ 'member' => $rating['userid']
+ ) );
+ }
+
+ /* Tag Prefix */
+ try
+ {
+ $prefix = $this->db->select( '*', 'prefix', array( "prefixid=?", $topic['prefixid'] ) )->first();
+ $lang = $this->db->select( '*', 'phrase', array( "varname=?", "prefix_{$prefix['prefixid']}_title_plain" ) )->first();
+
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $topic['forumid'],
+ 'tag_meta_id' => $topic['threadid'],
+ 'tag_text' => $lang['text'],
+ 'tag_member_id' => $topic['postuserid'],
+ 'tag_prefix' => 1, # key to this whole operation right here
+ ) );
+ }
+ catch( \UnderflowException $e ) {}
+
+ /* Tags */
+ if( $topic['taglist'] !== NULL AND !empty( $topic['taglist'] ) )
+ {
+ $tags = explode( ',', $topic['taglist'] );
+ if ( count( $tags ) )
+ {
+ foreach( $tags AS $tag )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $topic['forumid'],
+ 'tag_meta_id' => $topic['threadid'],
+ 'tag_text' => $tag,
+ 'tag_member_id' => $topic['postuserid'],
+ 'tag_prefix' => 0,
+ ) );
+ }
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $topic['threadid'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'postid' );
+
+ foreach( $this->fetch( 'post', 'postid' ) AS $post )
+ {
+ $info = array(
+ 'pid' => $post['postid'],
+ 'topic_id' => $post['threadid'],
+ 'post' => static::fixPostData( $post['pagetext'] ),
+ 'author_id' => $post['userid'],
+ 'author_name' => $post['username'],
+ 'ip_address' => $post['ipaddress'],
+ 'post_date' => $post['dateline'],
+ 'queued' => ( $post['visible'] >= 2 ) ? -1 : ( ( $post['visible'] == 0 ) ? 1 : 0 ),
+ 'post_htmlstate' => ( static::$isLegacy === FALSE AND in_array( $post['htmlstate'], array( 'on', 'on_nl2br' ) ) ) ? 1 : 0,
+ );
+
+ $post_id = $libraryClass->convertForumsPost( $info );
+
+ /* Reputation */
+ foreach( $this->db->select( '*', 'reputation', array( "postid=?", $post['postid'] ) ) AS $rep )
+ {
+ $libraryClass->convertReputation( array(
+ 'id' => $rep['reputationid'],
+ 'app' => 'forums',
+ 'type' => 'pid',
+ 'type_id' => $post['postid'],
+ 'member_id' => $rep['whoadded'],
+ 'member_received' => $rep['userid'],
+ 'rep_date' => $rep['dateline'],
+ 'rep_rating' => $rep['reputation']
+ ) );
+ }
+
+ /* Edit History */
+ $latestedit = 0;
+ $reason = NULL;
+ $name = NULL;
+ foreach( $this->db->select( '*', 'postedithistory', array( "postid=?", $post['postid'] ) ) AS $edit )
+ {
+ $libraryClass->convertEditHistory( array(
+ 'id' => $edit['postedithistoryid'],
+ 'class' => 'IPS\\forums\\Topic\\Post',
+ 'comment_id' => $post['postid'],
+ 'member' => $edit['userid'],
+ 'time' => $edit['dateline'],
+ 'old' => $edit['pagetext']
+ ) );
+
+ if ( $edit['dateline'] > $latestedit )
+ {
+ $latestedit = $edit['dateline'];
+ $reason = $edit['reason'];
+ $name = $edit['username'];
+ }
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'infraction', array( "postid=?", $post['postid'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['infractionid'],
+ 'wl_member' => $warn['userid'],
+ 'wl_moderator' => $warn['whoadded'],
+ 'wl_date' => $warn['dateline'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['note'],
+ 'wl_note_mods' => $warn['customreason'],
+ ) );
+ }
+
+ /* If we have a latest edit, then update the main post - this should really be in the library, as the converters should not be altering data */
+ if ( $latestedit )
+ {
+ \IPS\Db::i()->update( 'forums_posts', array( 'append_edit' => 1, 'edit_time' => $latestedit, 'edit_name' => $name, 'post_edit_reason' => $reason ), array( "pid=?", $post_id ) );
+ }
+
+ $libraryClass->setLastKeyValue( $post['postid'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'attachmentid' );
+
+ $where = NULL;
+ $column = NULL;
+
+ if ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) )
+ {
+ $where = array( "contenttypeid=?", static::$postContentType );
+ $column = 'contentid';
+ }
+ else
+ {
+ $column = 'postid';
+ }
+
+ foreach( $this->fetch( 'attachment', 'attachmentid', $where ) AS $attachment )
+ {
+ try
+ {
+ $topic_id = $this->db->select( 'threadid', 'post', array( "postid=?", $attachment[$column] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If the topic is missing, there isn't much we can do. */
+ $libraryClass->setLastKeyValue( $attachment['attachmentid'] );
+ continue;
+ }
+
+ if ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) )
+ {
+ try
+ {
+ $filedata = $this->db->select( '*', 'filedata', array( "filedataid=?", $attachment['filedataid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If the filedata row is missing, there isn't much we can do. */
+ $libraryClass->setLastKeyValue( $attachment['attachmentid'] );
+ continue;
+ }
+ }
+ else
+ {
+ $filedata = $attachment;
+ $filedata['filedataid'] = $attachment['attachmentid'];
+ }
+
+ $map = array(
+ 'id1' => $topic_id,
+ 'id2' => $attachment[$column]
+ );
+
+ $info = array(
+ 'attach_id' => $attachment['attachmentid'],
+ 'attach_file' => $attachment['filename'],
+ 'attach_date' => $attachment['dateline'],
+ 'attach_member_id' => $attachment['userid'],
+ 'attach_hits' => $attachment['counter'],
+ 'attach_ext' => $filedata['extension'],
+ 'attach_filesize' => $filedata['filesize'],
+ );
+
+ if ( $this->app->_session['more_info']['convertAttachments']['file_location'] == 'database' )
+ {
+ /* Simples! */
+ $data = $filedata['filedata'];
+ $path = NULL;
+ }
+ else
+ {
+ $data = NULL;
+ $path = implode( '/', preg_split( '//', $filedata['userid'], -1, PREG_SPLIT_NO_EMPTY ) );
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['file_location'], '/' ) . '/' . $path . '/' . $filedata['filedataid'] . '.attach';
+ }
+
+ $attach_id = $libraryClass->convertAttachment( $info, $map, $path, $data );
+
+ /* Do some re-jiggery on the post itself to make sure attachment displays */
+ if ( $attach_id !== FALSE )
+ {
+ try
+ {
+ $pid = $this->app->getLink( $attachment[$column], 'forums_posts' );
+
+ $post = \IPS\Db::i()->select( 'post', 'forums_posts', array( "pid=?", $pid ) )->first();
+
+ if ( preg_match( "/\[ATTACH(.+?)?\]".$attachment['attachmentid']."\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH(.+?)?\]" . $attachment['attachmentid'] . "\[\/ATTACH\]/i", '[attachment=' . $attach_id . ':name]', $post );
+
+ \IPS\Db::i()->update( 'forums_posts', array( 'post' => $post ), array( "pid=?", $pid ) );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \OutOfRangeException $e ) {}
+ }
+
+ $libraryClass->setLastKeyValue( $attachment['attachmentid'] );
+ }
+ }
+
+ /* !vBulletin Stuff */
+
+ /**
+ * @brief Silly Bitwise
+ */
+ public static $bitOptions = array (
+ 'forumoptions' => array(
+ 'active' => 1,
+ 'allowposting' => 2,
+ 'cancontainthreads' => 4,
+ 'moderatenewpost' => 8,
+ 'moderatenewthread' => 16,
+ 'moderateattach' => 32,
+ 'allowbbcode' => 64,
+ 'allowimages' => 128,
+ 'allowhtml' => 256,
+ 'allowsmilies' => 512,
+ 'allowicons' => 1024,
+ 'allowratings' => 2048,
+ 'countposts' => 4096,
+ 'canhavepassword' => 8192,
+ 'indexposts' => 16384,
+ 'styleoverride' => 32768,
+ 'showonforumjump' => 65536,
+ 'prefixrequired' => 131072,
+ 'allowvideos' => 262144,
+ 'bypassdp' => 524288,
+ 'displaywrt' => 1048576,
+ 'canreputation' => 2097152,
+ ),
+ );
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ /* If it looks like a VBSEO URL, rewrite it */
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], '.html' ) !== FALSE )
+ {
+ /* Paginated topics */
+ if( preg_match( "/\/(\d+)\-.+?\-(\d+)\.html/", $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $url = $url->setPath( preg_replace( "/\/(\d+)\-.+?\-(\d+)\.html/", "/showthread.php", $url->data[ \IPS\Http\Url::COMPONENT_PATH ] ) )->setQueryString( array( 't' => $matches[1], 'page' => $matches[2] ) );
+ \IPS\Request::i()->t = $matches[1];
+ \IPS\Request::i()->page = $matches[2];
+ }
+ elseif( preg_match( "/\/(\d+)\-.+?\.html/", $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $url = $url->setPath( preg_replace( "/\/(\d+)\-.+?\.html/", "/showthread.php", $url->data[ \IPS\Http\Url::COMPONENT_PATH ] ) )->setQueryString( 't', $matches[1] );
+ \IPS\Request::i()->t = $matches[1];
+ }
+ }
+
+ /* Forum URLs are the same across VB 3.8 and VB 4 */
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'forumdisplay.php' ) !== FALSE )
+ {
+ /* Forum URLs can be in one of 3 formats really...
+ * /forumdisplay.php/1-name
+ * /forums/1-name
+ * /forumdisplay.php?f=1
+ */
+ $path = $url->data[ \IPS\Http\Url::COMPONENT_PATH ];
+ if( mb_strpos( $path, 'forumdisplay.php' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->f ) )
+ {
+ $oldId = \IPS\Request::i()->f;
+ }
+ else
+ {
+ $queryStringPieces = explode( '-', mb_substr( $path, mb_strpos( $path, 'forumdisplay.php/' ) + mb_strlen( 'forumdisplay.php/' ) ) );
+ $oldId = $queryStringPieces[0];
+ }
+ }
+
+ if( isset( $oldId ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, array( 'forums', 'forums_forums' ) );
+ $item = \IPS\forums\Forum::load( $data );
+
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ }
+ elseif( preg_match( '#/forums/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'forums', 'forums_forums' ) );
+ $item = \IPS\forums\Forum::load( $data );
+
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ /* And then post URLs, simple */
+ elseif(
+ ( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showthread.php' ) !== FALSE OR
+ mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showpost.php' ) !== FALSE OR
+ preg_match( '#/threads/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ] )
+ )
+ AND isset( \IPS\Request::i()->p )
+ )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) \IPS\Request::i()->p, array( 'posts', 'forums_posts' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Try the main table */
+ $data = (string) $this->app->getLink( (int) \IPS\Request::i()->p, array( 'posts', 'forums_posts' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic\Post::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ /* And then topic URLs, same idea */
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showthread.php' ) !== FALSE )
+ {
+ /* Topic URLs can be in one of 3 formats really...
+ * /showthread.php/1-name
+ * /threads/1-name
+ * /showthread.php?t=1
+ */
+ $path = $url->data[ \IPS\Http\Url::COMPONENT_PATH ];
+ if( mb_strpos( $path, 'showthread.php' ) !== FALSE )
+ {
+ if( isset( \IPS\Request::i()->t ) )
+ {
+ $oldId = \IPS\Request::i()->t;
+ }
+ else
+ {
+ $queryStringPieces = explode( '-', mb_substr( $path, mb_strpos( $path, 'showthread.php/' ) + mb_strlen( 'showthread.php/' ) ) );
+ $oldId = $queryStringPieces[0];
+ }
+ }
+ elseif( preg_match( '#/forums/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $oldId = (int) $matches[1];
+ }
+
+ if( isset( $oldId ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Try the main table */
+ $data = (string) $this->app->getLink( $oldId, array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ }
+ elseif( preg_match( '#/threads/([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* Try the main table */
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ /* And finally, archives */
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'archive/index.php' ) !== FALSE )
+ {
+ $request = str_replace( '/archive/index.php/', '', $_SERVER['REQUEST_URI'] );
+ $parameters = explode( '-', $request );
+ $parameters[1] = str_replace( '.html', '', $parameters[1] );
+
+ try
+ {
+ switch( $parameters[0] )
+ {
+ case 't':
+ try
+ {
+ $data = $this->getLink( (string) $parameters[1], array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = $this->getLink( (string) $parameters[1], array( 'topics', 'forums_topics', FALSE, TRUE ) );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ break;
+
+ case 'f':
+ $data = $this->app->getLink( (string) $parameters[1], array( 'forums', 'forums_forums' ) );
+ $item = \IPS\forums\Forum::load( $data );
+
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ break;
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Vbulletin5.php b/applications/convert/sources/Software/Forums/Vbulletin5.php
new file mode 100644
index 00000000..b11020f0
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Vbulletin5.php
@@ -0,0 +1,733 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin5 extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin Forums (5.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin5";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'forum',
+ 'where' => NULL,
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'thread',
+ 'where' => NULL,
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'post',
+ 'where' => NULL,
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'attachment',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Count Source Rows for a specific step
+ *
+ * @param string $table The table containing the rows to count.
+ * @param array|NULL $where WHERE clause to only count specific rows, or NULL to count all.
+ * @param bool $recache Skip cache and pull directly (updating cache)
+ * @return integer
+ * @throws \IPS\convert\Exception
+ */
+ public function countRows( $table, $where=NULL, $recache=FALSE )
+ {
+ switch( $table )
+ {
+ case 'forum':
+ return count( $this->fetchForums() );
+ break;
+
+ case 'thread':
+ return parent::countRows( 'node', array( \IPS\Db::i()->in( 'contenttypeid', array( $this->fetchType( 'Text' ), $this->fetchType( 'Poll' ), $this->fetchType( 'Gallery' ), $this->fetchType( 'Video' ), $this->fetchType( 'Link' ) ) ) . " AND " . \IPS\Db::i()->in( 'parentid', array_keys( $this->fetchForums() ) ), $this->fetchType( 'Text' ), $this->fetchType( 'Poll' ) ) );
+ break;
+
+ case 'post':
+ return parent::countRows( 'node', array( \IPS\Db::i()->in( 'contenttypeid', array( $this->fetchType( 'Text' ), $this->fetchType( 'Gallery' ), $this->fetchType( 'Video' ), $this->fetchType( 'Link' ) ) ) ) );
+ break;
+
+ case 'attachment':
+ return parent::countRows( 'node', array( "contenttypeid=?", $this->fetchType( 'Attach' ) ) );
+ break;
+
+ default:
+ return parent::countRows( $table, $where, $recache );
+ break;
+ }
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin5' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ return array( 'f_rebuild_posts', 'f_recounting_forums', 'f_recounting_topics' );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin5::fixPostData( $post );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array( 'convertAttachments' );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'file_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'node.nodeid' );
+
+ foreach( $this->fetch( 'node', 'nodeid', array( "node.nodeid<>? AND closure.parent=? AND node.contenttypeid=?", 2, 2, $this->fetchType( 'Channel' ) ) )->join( 'closure', "closure.child=node.nodeid" ) AS $forum )
+ {
+ $self = $this;
+ $checkpermission = function( $name, $perm ) use ( $forum, $self )
+ {
+ $key = $name;
+ if ( $name == 'forumoptions' )
+ {
+ $key = 'nodeoptions';
+ }
+
+ if ( $forum[$key] & $self::$bitOptions[$name][$perm] )
+ {
+ return TRUE;
+ }
+
+ return FALSE;
+ };
+
+ $info = array(
+ 'id' => $forum['nodeid'],
+ 'name' => $forum['title'],
+ 'description' => $forum['description'],
+ 'topics' => $forum['textcount'],
+ 'posts' => $forum['totalcount'],
+ 'last_post' => $forum['lastcontent'],
+ 'last_poster_id' => $forum['lastauthorid'],
+ 'last_poster_name' => $forum['lastcontentauthor'],
+ 'parent_id' => $forum['parentid'],
+ 'position' => $forum['displayorder'],
+ 'preview_posts' => $checkpermission( 'forumoptions', 'moderatenewpost' ),
+ 'inc_postcount' => 1,
+ 'forum_allow_rating' => 1,
+ 'sub_can_post' => ( $forum['parentid'] == 2 ) ? 0 : 1,
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ $libraryClass->setLastKeyValue( $forum['nodeid'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'node.nodeid' );
+
+ foreach( $this->fetch( 'node', 'node.nodeid', array( \IPS\Db::i()->in( 'node.parentid', array_keys( $this->fetchForums() ) ) . " AND " . \IPS\Db::i()->in( 'node.contenttypeid', array( $this->fetchType( 'Text' ), $this->fetchType( 'Poll' ), $this->fetchType( 'Gallery' ), $this->fetchType( 'Video' ), $this->fetchType( 'Link' ) ) ) ) )->join( 'text', 'text.nodeid = node.nodeid' ) AS $topic )
+ {
+ /* Pesky Polls */
+ $poll = NULL;
+ $lastVote = 0;
+ try
+ {
+ /* Does this topic have a corresponding poll? */
+ $pollData = $this->db->select( '*', 'poll', array( "nodeid=?", $topic['nodeid'] ) )->first();
+ $pollOptions = @\unserialize( $pollData['options'] );
+ $lastVote = $pollData['lastvote'];
+
+ if ( $pollOptions === FALSE )
+ {
+ throw new \UnexpectedValueException;
+ }
+
+ $choices = array();
+ $numvotes = 0;
+ $votes = array();
+ foreach( $pollOptions AS $option )
+ {
+ $choices[$option['polloptionid']] = $option['title'];
+ $votes[$option['polloptionid']] = $option['votes'];
+ $numvotes += $option['votes'];
+ }
+
+ $poll = array();
+ $poll['poll_data'] = array(
+ 'pid' => $pollData['nodeid'],
+ 'choices' => array( 1 => array(
+ 'question' => $topic['title'],
+ 'multi' => $pollData['multiple'],
+ 'choice' => $choices,
+ 'votes' => $votes
+ ) ),
+ 'poll_question' => $topic['title'],
+ 'start_date' => $topic['publishdate'],
+ 'starter_id' => $topic['userid'],
+ 'votes' => $numvotes,
+ 'poll_view_voters' => $pollData['public']
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'pollvote', array( "nodeid=?", $pollData['nodeid'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[$vote['userid']] ) )
+ {
+ /* Create our structure - vB stores each individual vote as a different row whereas we combine them per user */
+ $ourVotes[$vote['userid']] = array( 'votes' => array() );
+ }
+
+ $ourVotes[$vote['userid']]['votes'][] = $vote['voteoption'];
+
+ /* These don't matter - just use the latest one */
+ $ourVotes[$vote['userid']]['vid'] = $vote['pollvoteid'];
+ $ourVotes[$vote['userid']]['vote_date'] = $vote['votedate'];
+ $ourVotes[$vote['userid']]['member_id'] = $vote['userid'];
+ }
+
+ /* Now we need to re-wrap it all for storage */
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][$member_id] = array(
+ 'vid' => $vote['vid'],
+ 'vote_date' => $vote['vote_date'],
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+ catch( \UnderflowException $e ) {} # if the poll is missing, don't bother
+ catch( \UnexpectedValueException $e ) {} # if poll data is corrupt, then skip
+
+ $info = array(
+ 'tid' => $topic['nodeid'],
+ 'title' => $topic['title'],
+ 'forum_id' => $topic['parentid'],
+ 'state' => $topic['open'] ? 'open' : 'closed',
+ 'posts' => $topic['totalcount'],
+ 'starter_id' => $topic['userid'],
+ 'start_date' => $topic['publishdate'],
+ 'last_poster_id' => $topic['lastauthorid'],
+ 'last_post' => $topic['lastcontent'],
+ 'starter_name' => $topic['authorname'],
+ 'last_poster_name' => $topic['lastcontentauthor'],
+ 'poll_state' => $poll,
+ 'last_vote' => $lastVote,
+ 'approved' => $topic['approved'],
+ 'pinned' => $topic['sticky'],
+ 'topic_open_time' => $topic['publishdate'],
+ 'topic_hiddenposts' => $topic['totalunpubcount']
+ );
+
+ unset( $poll );
+
+ $topic_id = $libraryClass->convertForumsTopic( $info );
+
+ $libraryClass->setLastKeyValue( $topic['nodeid'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'node.nodeid' );
+
+ foreach( $this->fetch( 'node', 'node.nodeid', array( \IPS\Db::i()->in( 'node.contenttypeid', array( $this->fetchType( 'Text' ), $this->fetchType( 'Gallery' ), $this->fetchType( 'Video' ), $this->fetchType( 'Link' ), $this->fetchType( 'Poll' ) ) ) ) )->join( 'text', 'text.nodeid = node.nodeid' ) AS $post )
+ {
+ /* Comments of comments of comments of comments */
+ $isSubComment = FALSE;
+ try
+ {
+ /* Is this the first post? */
+ $this->app->getLink( $post['parentid'], 'forums_forums' );
+
+ $post['parentid'] = $post['nodeid'];
+ }
+ catch( \OutOfRangeException $e )
+ {
+ try
+ {
+ /* If parentid exists as a forums_topics link, we don't actually need to do anything */
+ $this->app->getLink( $post['parentid'], 'forums_topics' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ try
+ {
+ $this->app->getLink( $post['parentid'], 'forums_posts' );
+
+ $isSubComment = TRUE;
+
+ /* Load the parent */
+ $parent = $this->db->select( '*', 'node', array( "node.nodeid=?", $post['parentid'] ) )->join( 'text', 'text.nodeid = node.nodeid' )->first();
+
+ $post['approved'] = $parent['approved'];
+ $post['parentid'] = $parent['parentid'];
+ $post['rawtext'] = "[quote name='" . $parent['authorname'] ."' timestamp='" . $parent['created'] . "']" . $parent['rawtext'] . "[/quote]\n" . $post['rawtext'];
+ }
+ catch( \OutOfRangeException $e ) # we need to go deeper!
+ {
+ /* Actually, we don't - end of the line */
+ $libraryClass->setLastKeyValue( $post['nodeid'] );
+ continue;
+ }
+ catch( \UnderflowException $e )
+ {
+ /* Loading the parent failed - move on */
+ $libraryClass->setLastKeyValue( $post['nodeid'] );
+ continue;
+ }
+ }
+ }
+ $info = array(
+ 'pid' => $post['nodeid'],
+ 'topic_id' => $post['parentid'],
+ 'post' => $post['rawtext'],
+ 'author_id' => $post['userid'],
+ 'author_name' => $post['authorname'],
+ 'ip_address' => $post['ipaddress'],
+ 'post_date' => $post['publishdate'],
+ 'queued' => $post['approved'] ? 0 : -1,
+ 'post_htmlstate' => ( in_array( $post['htmlstate'], array( 'on', 'on_nl2br' ) ) ) ? 1 : 0,
+ );
+
+ $post_id = $libraryClass->convertForumsPost( $info );
+
+ /* Reputation */
+ foreach( $this->db->select( '*', 'reputation', array( "nodeid=?", $post['nodeid'] ) ) AS $rep )
+ {
+ $libraryClass->convertReputation( array(
+ 'id' => $rep['reputationid'],
+ 'app' => 'forums',
+ 'type' => 'pid',
+ 'type_id' => $post['nodeid'],
+ 'member_id' => $rep['whoadded'],
+ 'member_received' => $rep['userid'],
+ 'rep_date' => $rep['dateline'],
+ 'rep_rating' => $rep['reputation']
+ ) );
+ }
+
+ /* Edit History */
+ $latestedit = 0;
+ $reason = NULL;
+ $name = NULL;
+ foreach( $this->db->select( '*', 'postedithistory', array( "nodeid=?", $post['nodeid'] ) ) AS $edit )
+ {
+ $libraryClass->convertEditHistory( array(
+ 'id' => $edit['postedithistoryid'],
+ 'class' => 'IPS\\forums\\Topic\\Post',
+ 'comment_id' => $post['nodeid'],
+ 'member' => $edit['userid'],
+ 'time' => $edit['dateline'],
+ 'old' => $edit['pagetext']
+ ) );
+
+ if ( $edit['dateline'] > $latestedit )
+ {
+ $latestedit = $edit['dateline'];
+ $reason = $edit['reason'];
+ $name = $edit['username'];
+ }
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'infraction', array( "nodeid=?", $post['nodeid'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['infractionid'],
+ 'wl_member' => $warn['nodeid'],
+ 'wl_moderator' => $warn['whoadded'],
+ 'wl_date' => $warn['dateline'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['note'],
+ 'wl_note_mods' => $warn['customreason'],
+ ) );
+ }
+
+ /* If we have a latest edit, then update the main post - this should really be in the library, as the converters should not be altering data */
+ if ( $latestedit )
+ {
+ \IPS\Db::i()->update( 'forums_posts', array( 'append_edit' => 1, 'edit_time' => $latestedit, 'edit_name' => $name, 'post_edit_reason' => $reason ), array( "pid=?", $post_id ) );
+ }
+
+ $libraryClass->setLastKeyValue( $post['nodeid'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'node.nodeid' );
+
+ $main = $this->fetch( 'node', 'node.nodeid', array( "node.contenttypeid=?", $this->fetchType( 'Attach' ) ) )
+ ->join( 'attach', 'attach.nodeid = node.nodeid' )
+ ->join( 'filedata', 'filedata.filedataid = attach.filedataid' );
+
+ foreach( $main AS $attachment )
+ {
+ try
+ {
+ $this->app->getLink( $attachment['parentid'], 'forums_topics' );
+ $post_id = $attachment['nodeid'];
+ }
+ catch( \OutOfRangeException $e )
+ {
+ try
+ {
+ $this->app->getLink( $attachment['parentid'], 'forums_posts' );
+
+ $parent = $this->db->select( 'nodeid, parentid', 'node', array( "nodeid=?", $attachment['parentid'] ) )->first();
+ $attachment['parentid'] = $parent['parentid'];
+ $post_id = $parent['nodeid'];
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $libraryClass->setLastKeyValue( $attachment['nodeid'] );
+ continue;
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $attachment['nodeid'] );
+ continue;
+ }
+ }
+
+ $map = array(
+ 'id1' => $attachment['parentid'],
+ 'id2' => $post_id
+ );
+
+ $info = array(
+ 'attach_id' => $attachment['nodeid'],
+ 'attach_file' => $attachment['filename'],
+ 'attach_date' => $attachment['dateline'],
+ 'attach_member_id' => $attachment['userid'],
+ 'attach_hits' => $attachment['counter'],
+ 'attach_ext' => $attachment['extension'],
+ 'attach_filesize' => $attachment['filesize'],
+ );
+
+ if ( $this->app->_session['more_info']['convertAttachments']['file_location'] == 'database' )
+ {
+ /* Simples! */
+ $data = $attachment['filedata'];
+ $path = NULL;
+ }
+ else
+ {
+ $data = NULL;
+ $path = implode( '/', preg_split( '//', $attachment['userid'], -1, PREG_SPLIT_NO_EMPTY ) );
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['file_location'], '/' ) . '/' . $path . '/' . $attachment['filedataid'] . '.attach';
+ }
+
+ $attach_id = $libraryClass->convertAttachment( $info, $map, $path, $data );
+
+ /* Do some re-jiggery on the post itself to make sure attachment displays */
+ if ( $attach_id !== FALSE )
+ {
+ try
+ {
+ $pid = $this->app->getLink( $post_id, 'forums_posts' );
+
+ $post = \IPS\Db::i()->select( 'post', 'forums_posts', array( "pid=?", $pid ) )->first();
+ }
+ catch( \OutOfRangeException $e )
+ {
+ /* If the post didn't exist and this is just an orphaned attachment, move along */
+ $libraryClass->setLastKeyValue( $attachment['nodeid'] );
+ continue;
+ }
+
+ if ( preg_match( "/\[ATTACH=(.+?)?\]n" . $attachment['nodeid'] . "\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH(.+?)?\]" . $attachment['nodeid'] . "\[\/ATTACH\]/i", '[attachment=' . $attach_id . ':name]', $post );
+
+ \IPS\Db::i()->update( 'forums_posts', array( 'post' => $post ), array( "pid=?", $pid ) );
+ }
+ else
+ {
+ $oldAttachId = NULL;
+
+ /* Try old attach ID */
+ try
+ {
+ $oldAttachId = $this->db->select( 'attachmentid', 'attachment', array( 'filedataid=?', $attachment['filedataid'] ) )->first();
+ }
+ /* Don't fail if this table/value doesn't exist */
+ catch( \Exception $e ) { }
+
+ if ( $oldAttachId !== NULL and preg_match( "/\[ATTACH=(.+?)?\]" . $oldAttachId . "\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH(.+?)?\]" . $oldAttachId . "\[\/ATTACH\]/i", '[attachment=' . $attach_id . ':name]', $post );
+
+ \IPS\Db::i()->update( 'forums_posts', array( 'post' => $post ), array( "pid=?", $pid ) );
+ }
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $attachment['nodeid'] );
+ }
+ }
+
+ /* !vBulletin Stuff */
+
+ /**
+ * @brief Silly Bitwise
+ */
+ public static $bitOptions = array (
+ 'forumoptions' => array(
+ 'active' => 1,
+ 'allowposting' => 2,
+ 'cancontainthreads' => 4,
+ 'moderatenewpost' => 8,
+ 'moderatenewthread' => 16,
+ 'moderateattach' => 32,
+ 'allowbbcode' => 64,
+ 'allowimages' => 128,
+ 'allowhtml' => 256,
+ 'allowsmilies' => 512,
+ 'allowicons' => 1024,
+ 'allowratings' => 2048,
+ 'countposts' => 4096,
+ 'canhavepassword' => 8192,
+ 'indexposts' => 16384,
+ 'styleoverride' => 32768,
+ 'showonforumjump' => 65536,
+ 'prefixrequired' => 131072,
+ 'allowvideos' => 262144,
+ 'bypassdp' => 524288,
+ 'displaywrt' => 1048576,
+ 'canreputation' => 2097152,
+ ),
+ );
+
+ /**
+ * Helper method to retrieve forums from nodes
+ *
+ * @return void
+ */
+ protected function fetchForums()
+ {
+ $forums = array();
+
+ foreach( $this->db->select( '*', 'node', array( "node.nodeid<>2 AND closure.parent=? AND node.contenttypeid=?", $this->fetchType( 'Thread' ), $this->fetchType( 'Channel' ) ) )->join( 'closure', "closure.child = node.nodeid" ) AS $node )
+ {
+ $forums[$node['nodeid']] = $node;
+ }
+
+ return $forums;
+ }
+
+ /**
+ * @brief Types Cache
+ */
+ protected $typesCache = array();
+
+ /**
+ * Helper method to retrieve content type ids
+ *
+ * @return void
+ */
+ protected function fetchType( $type )
+ {
+ if ( count( $this->typesCache ) )
+ {
+ return $this->typesCache[ $type ];
+ }
+ else
+ {
+ foreach( $this->db->select( '*', 'contenttype' ) AS $contenttype )
+ {
+ $this->typesCache[ $contenttype['class'] ] = $contenttype['contenttypeid'];
+ }
+
+ return $this->typesCache[ $type ];
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ * @note Forum URLs do not have IDs in them, so we cannot redirect them reliably
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#(?data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ) );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( (int) $matches[1], array( 'topics', 'forums_topics' ), FALSE, TRUE );
+ }
+ $item = \IPS\forums\Topic::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Forums/Xenforo.php b/applications/convert/sources/Software/Forums/Xenforo.php
new file mode 100644
index 00000000..9ecaae0b
--- /dev/null
+++ b/applications/convert/sources/Software/Forums/Xenforo.php
@@ -0,0 +1,716 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Forums;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenforo extends \IPS\convert\Software
+{
+ /**
+ * @brief Flag to indicate the post data has been fixed during conversion, and we only need to use Legacy Parser
+ */
+ public static $contentFixed = TRUE;
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "XenForo";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "xenforo";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertForumsForums' => array(
+ 'table' => 'xf_node',
+ 'where' => array( \IPS\Db::i()->in( 'node_type_id', array( 'Category', 'Forum', 'LinkForum' ) ) )
+ ),
+ 'convertForumsTopics' => array(
+ 'table' => 'xf_thread',
+ 'where' => NULL
+ ),
+ 'convertForumsPosts' => array(
+ 'table' => 'xf_post',
+ 'where' => NULL,
+ ),
+ 'convertAttachments' => array(
+ 'table' => 'xf_attachment',
+ 'where' => array( "content_type=?", 'post' )
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'xenforo' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertAttachments'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertAttachments':
+ $return['convertAttachments'] = array(
+ 'attach_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\forums\Forum', 'count' => 0 ), 5, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildContent', array( 'app' => $this->app->app_id, 'link' => 'forums_posts', 'class' => 'IPS\forums\Topic\Post' ), 2, array( 'app', 'link', 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\forums\Topic' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'convert', 'RebuildFirstPostIds', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'convert', 'DeleteEmptyTopics', array( 'app' => $this->app->app_id ), 4, array( 'app' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'forums_topics', 'class' => 'IPS\forums\Topic' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_forum_last_post_data", "f_rebuild_posts", "f_recounting_forums", "f_recounting_topics", "f_topic_tags_recount" );
+ }
+
+ /**
+ * Fix post data
+ *
+ * @param string raw post data
+ * @return string parsed post data
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Xenforo::fixPostData( $post );
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase
+ * @return string|null
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Convert forums
+ *
+ * @return void
+ */
+ public function convertForumsForums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'node_id' );
+
+ foreach( $this->fetch( 'xf_node', 'node_id', array( $this->db->in( 'node_type_id', array( 'Category', 'Forum', 'LinkForum' ) ) ) ) AS $row )
+ {
+ $data = array();
+ try
+ {
+ switch( $row['node_type_id'] )
+ {
+ case 'Forum':
+ $data = $this->db->select( '*', 'xf_forum', array( "node_id=?", $row['node_id'] ) )->first();
+ break;
+
+ case 'LinkForum':
+ $data = $this->db->select( '*', 'xf_link_forum', array( "node_id=?", $row['node_id'] ) )->first();
+ break;
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ $info = array(
+ 'id' => $row['node_id'],
+ 'name' => $row['title'],
+ 'description' => $row['description'],
+ 'topics' => ( isset( $data['discussion_count'] ) ) ? $data['discussion_count'] : 0,
+ 'posts' => ( isset( $data['message_count'] ) ) ? $data['message_count'] : 0,
+ 'last_post' => ( isset( $data['last_post_date'] ) ) ? $data['last_post_date'] : 0,
+ 'last_poster_id' => ( isset( $data['last_post_user_id'] ) ) ? $data['last_post_user_id'] : 0,
+ 'last_poster_name' => ( isset( $data['last_poster_name'] ) ) ? $data['last_post_username'] : '',
+ 'parent_id' => ( $row['parent_node_id'] == 0 ) ? -1 : $row['parent_node_id'],
+ 'position' => $row['display_order'],
+ 'last_title' => ( isset( $data['last_thread_title'] ) ) ? $data['last_thread_title'] : '',
+ 'allow_poll' => ( isset( $data['allow_poll'] ) ) ? $data['allow_poll'] : 0,
+ 'inc_postcount' => ( isset( $data['count_messages'] ) ) ? $data['count_messages'] : 0,
+ 'redirect_url' => ( isset( $data['link_url'] ) ) ? $data['link_url'] : NULL,
+ 'redirect_on' => ( $row['node_type_id'] == 'LinkForum' ) ? 1 : 0,
+ 'redirect_hits' => ( isset( $data['redirect_count'] ) ) ? $data['redirect_count'] : 0,
+ 'sub_can_post' => ( isset( $data['allow_posting'] ) ) ? $data['allow_posting'] : 0,
+ );
+
+ $libraryClass->convertForumsForum( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xf_forum_watch', array( "node_id=? AND notify_on=?", $row['node_id'], 'thread' ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'forum',
+ 'follow_rel_id' => $row['node_id'],
+ 'follow_rel_id_type' => 'forums_forums',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['send_alert'] OR $follow['send_email'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['node_id'] );
+ }
+ }
+
+ /**
+ * Convert topics
+ *
+ * @return void
+ */
+ public function convertForumsTopics()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'thread_id' );
+
+ foreach( $this->fetch( 'xf_thread', 'thread_id' ) AS $row )
+ {
+ /* Poll */
+ $poll = NULL;
+ if ( $row['discussion_type'] == 'poll' )
+ {
+ try
+ {
+ $poll_data = $this->db->select( '*', 'xf_poll', array( "content_type=? AND content_id=?", 'thread', $row['thread_id'] ) )->first();
+
+ $choices = array();
+ $votes = array();
+ $index = 1;
+ foreach( $this->db->select( '*', 'xf_poll_response', array( "poll_id=?", $poll_data['poll_id'] ) ) AS $choice )
+ {
+ $choices[$index] = $choice['response'];
+ $votes[$index] = $choice['response_vote_count'];
+ $search[$index] = $choice['poll_response_id'];
+ $index++;
+ }
+
+ $poll['poll_data'] = array(
+ 'pid' => $poll_data['poll_id'],
+ 'choices' => array( 1 => array(
+ 'question' => $poll_data['question'],
+ 'multi' => ( $poll_data['max_votes'] > 1 ) ? 1 : 0,
+ 'choice' => $choices,
+ 'votes' => $votes
+ ) ),
+ 'poll_question' => $poll_data['question'],
+ 'start_date' => $row['post_date'],
+ 'starter_id' => $row['user_id'],
+ 'votes' => array_sum( $votes ),
+ 'poll_view_voters' => $poll_data['public_votes']
+ );
+
+ $poll['vote_data'] = array();
+ $ourVotes = array();
+ foreach( $this->db->select( '*', 'xf_poll_vote', array( "poll_id=?", $poll_data['poll_id'] ) ) AS $vote )
+ {
+ if ( !isset( $ourVotes[$vote['user_id']] ) )
+ {
+ $ourVotes[$vote['user_id']] = array( 'votes' => array() );
+ }
+
+ $ourVotes[$vote['user_id']]['votes'][] = array_search( $vote['poll_response_id'], $search );
+ $ourVotes[$vote['user_id']]['member_id'] = $vote['user_id'];
+ }
+
+ foreach( $ourVotes AS $member_id => $vote )
+ {
+ $poll['vote_data'][$member_id] = array(
+ 'member_id' => $vote['member_id'],
+ 'member_choices' => array( 1 => $vote['votes'] ),
+ );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ /* Approval */
+ switch( $row['discussion_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderation':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ /* Moved To */
+ $moved_to = NULL;
+ if ( $row['discussion_type'] == 'redirect' )
+ {
+ try
+ {
+ $redirect = $this->db->select( '*', 'xf_thread_redirect', array( "thread_id=?", $row['thread_id'] ) )->first();
+ $key = explode( '-', $redirect['redirect_key'] );
+ $moved_to = array( $key[1], $key[2] );
+ }
+ catch( \UnderflowException $e ) {}
+ }
+
+ $info = array(
+ 'tid' => $row['thread_id'],
+ 'title' => $row['title'],
+ 'forum_id' => $row['node_id'],
+ 'state' => ( $row['discussion_open'] ) ? 'open' : 'closed',
+ 'posts' => $row['reply_count'],
+ 'starter_id' => $row['user_id'],
+ 'start_date' => $row['post_date'],
+ 'last_poster_id' => $row['last_post_user_id'],
+ 'last_post' => $row['last_post_date'],
+ 'starter_name' => $row['username'],
+ 'last_poster_name' => $row['last_post_username'],
+ 'poll_state' => $poll,
+ 'views' => $row['view_count'],
+ 'approved' => $approved,
+ 'pinned' => $row['sticky'],
+ 'moved_to' => $moved_to,
+ );
+
+ $libraryClass->convertForumsTopic( $info );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xf_thread_watch', array( "thread_id=?", $row['thread_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'forums',
+ 'follow_area' => 'topic',
+ 'follow_rel_id' => $row['thread_id'],
+ 'follow_rel_id_type' => 'forums_topics',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['email_subscribe'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ /* Tag Prefix */
+ if ( $row['prefix_id'] )
+ {
+ $prefix = $this->getPhrase( "thread_prefix_{$row['prefix_id']}" );
+
+ if ( !is_null( $prefix ) )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $row['node_id'],
+ 'tag_meta_id' => $row['thread_id'],
+ 'tag_text' => $prefix,
+ 'tag_member_id' => $row['user_id'],
+ 'tag_prefix' => 1, # key to this whole operation right here
+ ) );
+ }
+ }
+
+ /* Other tags */
+ if ( $row['tags'] )
+ {
+ $tags = \unserialize( $row['tags'] );
+ if ( count( $tags ) )
+ {
+ foreach( $tags AS $key => $tag )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'forums',
+ 'tag_meta_area' => 'forums',
+ 'tag_meta_parent_id' => $row['node_id'],
+ 'tag_meta_id' => $row['thread_id'],
+ 'tag_text' => $tag['tag'],
+ 'tag_member_id' => $row['user_id'],
+ 'tag_prefix' => 0
+ ) );
+ }
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $row['thread_id'] );
+ }
+ }
+
+ /**
+ * Convert posts
+ *
+ * @return void
+ */
+ public function convertForumsPosts()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'post_id' );
+
+ foreach( $this->fetch( 'xf_post', 'post_id' ) AS $row )
+ {
+ /* Query for the IP */
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+
+ /* Figure out queued stuff */
+ try
+ {
+ $topic_state = $this->db->select( 'discussion_state', 'xf_thread', array( "thread_id=?", $row['thread_id'] ) )->first();
+
+ if ( $topic_state == 'deleted' )
+ {
+ $row['message_state'] = 'topic_hidden';
+ }
+ }
+ catch( \UnderflowException $e ) {}
+
+ switch( $row['message_state'] )
+ {
+ case 'visible':
+ $queued = 0;
+ break;
+
+ case 'moderated':
+ $queued = 1;
+ break;
+
+ case 'deleted':
+ $queued = -1;
+ break;
+
+ case 'topic_hidden':
+ $queued = 2;
+ break;
+ }
+
+ $info = array(
+ 'pid' => $row['post_id'],
+ 'topic_id' => $row['thread_id'],
+ 'post' => static::fixPostData( $row['message'] ),
+ 'edit_time' => $row['last_edit_date'],
+ 'author_id' => $row['user_id'],
+ 'author_name' => $row['username'],
+ 'ip_address' => $ip,
+ 'post_date' => $row['post_date'],
+ 'queued' => $queued,
+ );
+
+ $post_id = $libraryClass->convertForumsPost( $info );
+
+ /* Reputation */
+ $likes = \unserialize( $row['like_users'] );
+ if ( count( $likes ) )
+ {
+ foreach( $likes AS $like )
+ {
+ $libraryClass->convertReputation( array(
+ 'app' => 'forums',
+ 'type' => 'pid',
+ 'type_id' => $row['post_id'],
+ 'member_id' => $like['user_id'],
+ 'member_received' => $row['user_id'],
+ ) );
+ }
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'xf_warning', array( "content_type=? AND content_id=?", 'post', $row['post_id'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['warning_id'],
+ 'wl_member' => $warn['user_id'],
+ 'wl_moderator' => $warn['warning_user_id'],
+ 'wl_date' => $warn['warning_date'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['title'],
+ 'wl_note_mods' => $warn['notes'],
+ ) );
+ }
+
+ /* Edit History */
+ $latestedit = 0;
+ $reason = NULL;
+ $name = NULL;
+ foreach( $this->db->select( '*', 'xf_edit_history', array( "content_type=? AND content_id=?", 'post', $row['post_id'] ) ) AS $edit )
+ {
+ $libraryClass->convertEditHistory( array(
+ 'id' => $edit['edit_history_id'],
+ 'class' => 'IPS\\forums\\Topic\\Post',
+ 'comment_id' => $row['post_id'],
+ 'member' => $edit['edit_user_id'],
+ 'time' => $edit['edit_date'],
+ 'old' => $edit['old_text']
+ ) );
+
+ if ( $edit['edit_date'] > $latestedit )
+ {
+ $latestedit = $edit['edit_date'];
+ }
+ }
+
+ /* If we have a latest edit, then update the main post - this should really be in the library, as the converters should not be altering data */
+ if ( $latestedit )
+ {
+ \IPS\Db::i()->update( 'forums_posts', array( 'append_edit' => 1, 'edit_time' => $latestedit ), array( "pid=?", $post_id ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['post_id'] );
+ }
+ }
+
+ /**
+ * Convert attachments
+ *
+ * @return void
+ */
+ public function convertAttachments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'xf_attachment.attachment_id' );
+
+ $it = $this->fetch( 'xf_attachment', 'xf_attachment.attachment_id', array( "content_type=?", 'post' ) )->join( 'xf_attachment_data', 'xf_attachment.data_id = xf_attachment_data.data_id' );
+
+ foreach( $it AS $row )
+ {
+ $topic = $this->db->select( 'thread_id', 'xf_post', array( "post_id=?", $row['content_id'] ) )->first();
+
+ $map = array(
+ 'id1' => $topic,
+ 'id2' => $row['content_id'],
+ );
+
+ $ext = explode( '.', $row['filename'] );
+ $ext = array_pop( $ext );
+
+ $info = array(
+ 'attach_id' => $row['attachment_id'],
+ 'attach_file' => $row['filename'],
+ 'attach_date' => $row['upload_date'],
+ 'attach_member_id' => $row['user_id'],
+ 'attach_hits' => $row['view_count'],
+ 'attach_ext' => $ext,
+ 'attach_filesize' => $row['file_size'],
+ );
+
+ $physical_name = $row['data_id'] . '-' . $row['file_hash'] . '.data';
+ $group = floor( $row['data_id'] / 1000 );
+ $path = rtrim( $this->app->_session['more_info']['convertAttachments']['attach_location'], '/' ) . '/' . $group . '/' . $physical_name;
+
+ $attachId = $libraryClass->convertAttachment( $info, $map, $path );
+
+ /* Update Post if we can */
+ try
+ {
+ if ( $attachId !== FALSE )
+ {
+ $ourAttachment = \IPS\File::get( 'core_Attachment', \IPS\Db::i()->select( 'attach_location', 'core_attachments', array( 'attach_id=?', $attachId ) )->first() );
+
+ $pid = $this->app->getLink( $row['content_id'], 'forums_posts' );
+
+ $post = \IPS\Db::i()->select( 'post', 'forums_posts', array( "pid=?", $pid ) )->first();
+
+ if ( preg_match( "/\[ATTACH(.+?)?\]" . $row['attachment_id'] . "\[\/ATTACH\]/i", $post ) )
+ {
+ $post = preg_replace( "/\[ATTACH(.+?)?\]" . $row['attachment_id'] . "\[\/ATTACH\]/i", '
', $post );
+ }
+ else
+ {
+ $post .= '
';
+ }
+
+ \IPS\Db::i()->update( 'forums_posts', array( 'post' => $post ), array( "pid=?", $pid ) );
+ }
+ }
+ catch( \UnderflowException $e ) {}
+ catch( \OutOfRangeException $e ) {}
+
+ $libraryClass->setLastKeyValue( $row['attachment_id'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( preg_match( '#/(forums|threads)/(.+?)\.([0-9]+)#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $oldId = (int) $matches[3];
+
+ switch( $matches[1] )
+ {
+ case 'forums':
+ $class = '\IPS\forums\Forum';
+ $types = array( 'forums', 'forums_forums' );
+ break;
+
+ case 'threads':
+ $class = '\IPS\forums\Topic';
+ $types = array( 'topics', 'forums_topics' );
+ break;
+ }
+ }
+ elseif( preg_match( '#/posts/([0-9]+)/#i', $url->data[ \IPS\Http\Url::COMPONENT_PATH ], $matches ) )
+ {
+ $class = '\IPS\forums\Topic\Post';
+ $types = array( 'posts', 'forums_posts' );
+ $oldId = $matches[1];
+ }
+
+ if( isset( $class ) )
+ {
+ try
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( $oldId, $types );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = (string) $this->app->getLink( $oldId, $types, FALSE, TRUE );
+ }
+ $item = $class::load( $data );
+
+ if( $item instanceof \IPS\Content\Item )
+ {
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( $item instanceof \IPS\Node\Model )
+ {
+ if( $item->can( 'view' ) )
+ {
+ return $item->url();
+ }
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Gallery/Coppermine.php b/applications/convert/sources/Software/Gallery/Coppermine.php
new file mode 100644
index 00000000..afb7268a
--- /dev/null
+++ b/applications/convert/sources/Software/Gallery/Coppermine.php
@@ -0,0 +1,311 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Gallery;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Coppermine extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Coppermine";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "coppermine";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGalleryCategories' => array(
+ 'table' => 'categories',
+ 'where' => NULL
+ ),
+ 'convertGalleryAlbums' => array(
+ 'table' => 'albums',
+ 'where' => NULL,
+ ),
+ 'convertGalleryImages' => array(
+ 'table' => 'pictures',
+ 'where' => NULL,
+ ),
+ 'convertGalleryComments' => array(
+ 'table' => 'comments',
+ 'where' => NULL,
+ )
+ );
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return NULL|array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'phpbb' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGalleryAlbums',
+ 'convertGalleryImages'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Method name
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertGalleryAlbums':
+ $options = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'gallery_categories' ), 'IPS\gallery\Category' ) AS $category )
+ {
+ $options[$category->_id] = $category->_title;
+ }
+
+ $return['convertGalleryAlbums']['members_gallery_category'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(
+ 'options' => $options
+ ),
+ 'field_hint' => NULL,
+ );
+ break;
+ case 'convertGalleryImages':
+ $return['convertGalleryImages']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack( 'convert_coppermine_filehint' ),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildGalleryImages', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\gallery\Image' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Album', 'count' => 0 ), 4, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Category', 'count' => 0 ), 5, array( 'class' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'gallery_images', 'class' => 'IPS\gallery\Image' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_gallery_images_rebuild", "f_gallery_cat_recount", "f_gallery_album_recount", "f_gallery_image_recount", "f_image_tags_recount" );
+ }
+
+ /**
+ * Convert categories
+ *
+ * @return void
+ */
+ public function convertGalleryCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'cid' );
+
+ foreach( $this->fetch( 'categories', 'cid' ) AS $row )
+ {
+ $libraryClass->convertGalleryCategory( array(
+ 'category_id' => $row['cid'],
+ 'category_name' => $row['name'],
+ 'category_desc' => $row['description'],
+ 'category_parent_id' => $row['parent'],
+ 'category_position' => $row['pos']
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['cid'] );
+ }
+ }
+
+ /**
+ * Convert albums
+ *
+ * @return void
+ */
+ public function convertGalleryAlbums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'aid' );
+
+ foreach( $this->fetch( 'albums', 'aid' ) AS $row )
+ {
+ $info = array(
+ 'album_id' => $row['aid'],
+ 'album_owner_id' => isset( $row['owner'] ) ? $row['owner'] : NULL,
+ 'album_category_id' => $row['category'],
+ 'album_description' => $row['description'],
+ 'album_position' => $row['pos'],
+ 'album_name' => $row['title'],
+ 'album_allow_comments' => ( $row['comments'] == 'YES' ),
+ 'album_allow_reviews' => ( $row['comments'] == 'YES' ),
+ 'album_allow_rating' => ( $row['votes'] == 'YES' )
+ );
+
+ $category = NULL;
+ try
+ {
+ $this->app->getLink( $row['category'], 'gallery_categories' );
+ }
+ catch( \Exception $e )
+ {
+ $category = $this->app->_session['more_info']['convertGalleryAlbums']['members_gallery_category'];
+ if ( $category == 0 )
+ {
+ $category = NULL;
+ }
+ }
+
+ $libraryClass->convertGalleryAlbum( $info, NULL, $category );
+ $libraryClass->setLastKeyValue( $row['aid'] );
+ }
+ }
+
+ /**
+ * Convert images
+ *
+ * @return void
+ */
+ public function convertGalleryImages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'pid' );
+
+ foreach( $this->fetch( 'pictures', 'pid' ) AS $row )
+ {
+ $info = array(
+ 'image_id' => $row['pid'],
+ 'image_album_id' => $row['aid'],
+ 'image_member_id' => $row['owner_id'],
+ /* Some images in the test data don't have titles, use filename if title is missing */
+ 'image_caption' => $row['title'] ?: $row['filename'],
+ 'image_description' => $row['caption'],
+ 'image_views' => $row['hits'],
+ 'image_date' => $row['ctime'],
+ 'image_updated' => \strtotime( $row['mtime'] ),
+ 'image_ipaddress' => $row['pic_raw_ip'],
+ 'image_file_name' => $row['filename']
+ );
+
+ /* Filepath */
+ $path = rtrim( $this->app->_session['more_info']['convertGalleryImages']['file_location'], '/' ) . '/' . $row['filepath'] . '/' . $row['filename'];
+
+ $libraryClass->convertGalleryImage( $info, $path );
+
+ /* Tags */
+ if( !empty( $row['keywords'] ) )
+ {
+ $keywords = explode( ' ', $row['keywords'] );
+ foreach( $keywords as $key )
+ {
+ $word = explode( ',', $key );
+
+ foreach( $word as $w )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'gallery',
+ 'tag_meta_area' => 'gallery',
+ 'tag_meta_parent_id' => $row['aid'],
+ 'tag_meta_id' => $row['pid'],
+ 'tag_text' => $w,
+ 'tag_prefix' => 0,
+ 'tag_member_id' => $row['owner_id']
+ ) );
+ }
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $row['pid'] );
+ }
+ }
+
+ /**
+ * Convert comments
+ *
+ * @return void
+ */
+ public function convertGalleryComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'msg_id' );
+
+ foreach( $this->fetch( 'comments', 'msg_id' ) AS $row )
+ {
+ $libraryClass->convertGalleryComment( array(
+ 'comment_id' => $row['msg_id'],
+ 'comment_text' => $row['msg_body'],
+ 'comment_img_id' => $row['pid'],
+ 'comment_author_id' => $row['author_id'],
+ 'comment_author_name' => $row['msg_author'],
+ 'comment_ip_address' => $row['msg_raw_ip'],
+ 'comment_post_date' => \strtotime( $row['msg_date'] ),
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['msg_id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Gallery/Photoplog.php b/applications/convert/sources/Software/Gallery/Photoplog.php
new file mode 100644
index 00000000..7478ce0e
--- /dev/null
+++ b/applications/convert/sources/Software/Gallery/Photoplog.php
@@ -0,0 +1,305 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 02 October 2016
+ */
+
+namespace IPS\convert\Software\Gallery;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Photoplog extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "PhotoPlog (vBulletin 3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "photoplog";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGalleryCategories' => array(
+ 'table' => 'categories',
+ 'where' => NULL
+ ),
+ 'convertGalleryAlbums'=> array(
+ 'table' => 'useralbums',
+ 'where' => NULL,
+ ),
+ 'convertGalleryImages' => array(
+ 'table' => 'fileuploads',
+ 'where' => NULL
+ ),
+ 'convertGalleryComments' => array(
+ 'table' => 'ratecomment',
+ 'where' => array( "rating=?", 0 )
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return NULL|array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGalleryAlbums',
+ 'convertGalleryImages'
+ );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return string Message to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildGalleryImages', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\gallery\Image' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Album', 'count' => 0 ), 4, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Category', 'count' => 0 ), 5, array( 'class' ) );
+
+ return array( "f_gallery_images_rebuild", "f_gallery_cat_recount", "f_gallery_album_recount", "f_gallery_image_recount" );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Method name
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertGalleryAlbums':
+ $options = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'gallery_categories' ), 'IPS\gallery\Category' ) AS $category )
+ {
+ $options[ $category->_id ] = $category->_title;
+ }
+
+ $return['convertGalleryAlbums']['members_gallery_category'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(
+ 'options' => $options
+ ),
+ 'field_hint' => NULL,
+ );
+ break;
+ case 'convertGalleryImages':
+ $return['convertGalleryImages']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_photoplog_hint'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Convert Photoplog Albums
+ *
+ * @return void
+ */
+ public function convertGalleryAlbums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'albumid' );
+
+ foreach( $this->fetch( 'useralbums', 'albumid' ) AS $row )
+ {
+ $info = array(
+ 'album_id' => $row['albumid'],
+ 'album_owner_id' => $row['userid'],
+ 'album_description' => $row['description'],
+ 'album_name' => $row['title'],
+ 'album_type' => $row['visible'] ?: 0
+ );
+
+ $category = $this->app->_session['more_info']['convertGalleryAlbums']['members_gallery_category'];
+ if ( $category == 0 )
+ {
+ $category = NULL;
+ }
+
+ $libraryClass->convertGalleryAlbum( $info, NULL, $category );
+
+ $libraryClass->setLastKeyValue( $row['albumid'] );
+ }
+ }
+
+ /**
+ * Convert Photoplog Categories
+ *
+ * @return void
+ */
+ public function convertGalleryCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'catid' );
+
+ foreach( $this->fetch( 'categories', 'catid' ) AS $row )
+ {
+ $libraryClass->convertGalleryCategory( array(
+ 'category_id' => $row['catid'],
+ 'category_name' => $row['title'],
+ 'category_desc' => $row['description'],
+ 'category_parent_id' => $row['parentid'] > 0 ? $row['parentid'] : 0,
+ 'category_position' => $row['displayorder']
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['catid'] );
+ }
+
+ /**
+ * Convert Photoplog Images
+ *
+ * @return void
+ */
+ public function convertGalleryImages()
+ {
+ $libraryClass = $this->getLibrary();
+ $libraryClass::setKey( 'fileid' );
+
+ foreach( $this->fetch( 'fileuploads', 'fileid' ) AS $row )
+ {
+ $album = \unserialize( $row['albumids'] );
+ $albumId = NULL;
+
+ if( is_array( $album ) )
+ {
+ $albumId = $album[0];
+ }
+
+ $info = array(
+ 'image_id' => $row['fileid'],
+ 'image_album_id' => $albumId,
+ 'image_category_id' => $row['catid'],
+ 'image_member_id' => $row['userid'],
+ 'image_caption' => $row['title'],
+ 'image_views' => $row['views'],
+ 'image_comments' => $row['num_comments0'] + $row['num_comments1'],
+ 'image_ratings_total' => $row['sum_ratings1'],
+ 'image_ratings_count' => $row['num_ratings1'],
+ 'image_rating' => $row['num_ratings1'] > 0 ? intval( $row['sum_ratings1'] / $row['num_ratings1'] ) : 0,
+ 'image_date' => $row['dateline'],
+ 'image_metadata' => json_encode( \unserialize( $row['exifinfo'] ) ),
+ 'image_file_name' => $row['filename'],
+ 'image_description' => $row['description'],
+ 'image_approved' => $row['moderate'] == 1 ? 0 : 1,
+ );
+
+ $libraryClass->convertGalleryImage( $info, rtrim( $this->app->_session['more_info']['convertGalleryImages']['file_location'], '/' ) . '/' . $row['userid'] . '/' . $row['filename'] );
+
+ /* Ratings! */
+ foreach( $this->db->select( '*', 'ratecomment', array( "fileid=? AND rating>0", $row['fileid'] ) ) AS $rating )
+ {
+ $libraryClass->convertRating( array(
+ 'id' => $rating['commentid'],
+ 'class' => 'IPS\gallery\Image',
+ 'item_link' => 'gallery_images',
+ 'item_id' => $row['fileid'],
+ 'rating' => $rating['rating'],
+ 'member' => $rating['userid'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['fileid'] );
+ }
+ }
+
+ /**
+ * Convert Photoplog Comments
+ *
+ * @return void
+ */
+ public function convertGalleryComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'commentid' );
+
+ foreach( $this->fetch( 'ratecomment', 'commentid', array( "rating=?", 0 ) ) AS $row )
+ {
+ $libraryClass->convertGalleryComment( array(
+ 'comment_id' => $row['commentid'],
+ 'comment_text' => $row['comment'],
+ 'comment_img_id' => $row['fileid'],
+ 'comment_author_id' => $row['userid'],
+ 'comment_author_name' => $row['username'],
+ 'comment_post_date' => $row['dateline'],
+ 'comment_approved' => ( $row['moderate'] == 1 ) ? 0 : 1,
+ ) );
+
+ $libraryClass->setLastKeyValue( $row['commentid'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Gallery/Photopost.php b/applications/convert/sources/Software/Gallery/Photopost.php
new file mode 100644
index 00000000..8cc882a6
--- /dev/null
+++ b/applications/convert/sources/Software/Gallery/Photopost.php
@@ -0,0 +1,102 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 6 December 2016
+ * @note Only redirect scripts are supported right now
+ */
+
+namespace IPS\convert\Software\Gallery;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Photopost extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "Photopost";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "photopost";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return NULL
+ */
+ public static function canConvert()
+ {
+ return NULL;
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ if( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showgallery.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->cat, 'gallery_categories' );
+ $item = \IPS\gallery\Category::load( $data );
+
+ if( $item->can('view') )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'showphoto.php' ) !== FALSE )
+ {
+ try
+ {
+ $data = (string) $this->app->getLink( \IPS\Request::i()->photo, 'gallery_images' );
+ $item = \IPS\gallery\Image::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Gallery/Vbulletin.php b/applications/convert/sources/Software/Gallery/Vbulletin.php
new file mode 100644
index 00000000..8e6b8dc3
--- /dev/null
+++ b/applications/convert/sources/Software/Gallery/Vbulletin.php
@@ -0,0 +1,481 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Gallery;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Vbulletin extends \IPS\convert\Software
+{
+ /**
+ * @brief vBulletin 4 Stores all attachments under one table - this will store the content type for the forums app.
+ */
+ protected static $imageContentType = NULL;
+
+ /**
+ * @brief The schematic for vB3 and vB4 is similar enough that we can make specific concessions in a sinle converter for either version.
+ */
+ protected static $isLegacy = NULL;
+
+ /**
+ * Constructor
+ *
+ * @param \IPS\convert\App The application to reference for database and other information.
+ * @param bool Establish a DB connection
+ * @return void
+ * @throws \InvalidArgumentException
+ */
+ public function __construct( \IPS\convert\App $app, $needDB=TRUE )
+ {
+ $return = parent::__construct( $app, $needDB );
+
+ /* Is this vB3 or vB4? */
+ if ( $needDB )
+ {
+ try
+ {
+ if ( static::$isLegacy === NULL )
+ {
+ $version = $this->db->select( 'value', 'setting', array( "varname=?", 'templateversion' ) )->first();
+
+ if ( mb_substr( $version, 0, 1 ) == '3' )
+ {
+ static::$isLegacy = TRUE;
+ }
+ else
+ {
+ static::$isLegacy = FALSE;
+ }
+ }
+
+
+ /* If this is vB4, what is the content type ID for posts? */
+ if ( static::$imageContentType === NULL AND ( static::$isLegacy === FALSE OR is_null( static::$isLegacy ) ) )
+ {
+ static::$imageContentType = $this->db->select( 'contenttypeid', 'contenttype', array( "class=?", 'Album' ) )->first();
+ }
+ }
+ catch( \Exception $e ) {}
+ }
+
+ return $return;
+ }
+
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "vBulletin Gallery (3.x/4.x)";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "vbulletin";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ $imageWhere = NULL;
+ $imageTable = 'picture';
+
+ if ( static::$isLegacy == FALSE )
+ {
+ $imageWhere = array( "contenttypeid=?", static::$imageContentType );
+ $imageTable = 'attachment';
+ }
+
+ return array(
+ 'convertGalleryAlbums'=> array(
+ 'table' => 'album',
+ 'where' => NULL,
+ ),
+ 'convertGalleryImages' => array(
+ 'table' => $imageTable,
+ 'where' => $imageWhere
+ ),
+ 'convertGalleryComments' => array(
+ 'table' => 'picturecomment',
+ 'where' => NULL
+ )
+ );
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'vbulletin' ) );
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildGalleryImages', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\gallery\Image' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Album', 'count' => 0 ), 4, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Category', 'count' => 0 ), 5, array( 'class' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'gallery_images', 'class' => 'IPS\gallery\Image' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_gallery_images_rebuild", "f_gallery_cat_recount", "f_gallery_album_recount", "f_gallery_image_recount", "f_image_tags_recount" );
+ }
+
+ /**
+ * Fix Post Data
+ *
+ * @param string Post
+ * @return string Fixed Posts
+ */
+ public static function fixPostData( $post )
+ {
+ return \IPS\convert\Software\Core\Vbulletin::fixPostData( $post );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertGalleryImages':
+ $return['convertGalleryImages'] = array(
+ 'file_location' => array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Radio',
+ 'field_default' => 'database',
+ 'field_required' => TRUE,
+ 'field_extra' => array(
+ 'options' => array(
+ 'database' => \IPS\Member::loggedIn()->language()->addToStack( 'database' ),
+ 'file_system' => \IPS\Member::loggedIn()->language()->addToStack( 'file_system' ),
+ ),
+ 'userSuppliedInput' => 'file_system',
+ ),
+ 'field_hint' => NULL,
+ )
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array( 'convertGalleryImages' );
+ }
+
+ /**
+ * Convert gallery albums
+ *
+ * @return void
+ */
+ public function convertGalleryAlbums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'albumid' );
+
+ foreach( $this->fetch( 'album', 'albumid' ) AS $album )
+ {
+ $socialgroup = NULL;
+ if ( $album['state'] == 'private' )
+ {
+ /* Fetch users friends */
+ $friends = array();
+ foreach( $this->db->select( 'relationid', 'userlist', array( "userid=?", $album['userid'] ) ) AS $friend )
+ {
+ $friends[$friend] = $friend;
+ }
+
+ $socialgroup = array( 'members' => $friends );
+ }
+
+ $info = array(
+ 'album_id' => $album['albumid'],
+ 'album_owner_id' => $album['userid'],
+ 'album_name' => $album['title'],
+ 'album_description' => $album['description'],
+ 'album_type' => ( $album['state'] == 'private' ) ? 3 : 1,
+ 'album_count_imgs' => $album['visible'],
+ 'album_count_imgs_hidden' => $album['moderation'],
+ 'album_last_img_date' => $album['lastpicturedate']
+ );
+
+ $libraryClass->convertGalleryAlbum( $info, $socialgroup );
+
+ $libraryClass->setLastKeyValue( $album['albumid'] );
+ }
+ }
+
+ /**
+ * Convert gallery images
+ *
+ * @return void
+ */
+ public function convertGalleryImages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ /* Don't even bother trying to swap things out - just do different things based on version */
+ if ( static::$isLegacy === TRUE )
+ {
+ $libraryClass::setKey( 'pictureid' );
+
+ foreach( $this->fetch( 'picture', 'pictureid' ) AS $image )
+ {
+ $filedata = NULL;
+ $filepath = NULL;
+
+ if ( $this->app->_session['more_info']['convertGalleryImages']['file_location'] == 'database' )
+ {
+ /* Simples! */
+ $filedata = $image['filedata'];
+ }
+ else
+ {
+ $filepath = floor( $image['pictureid'] / 1000 );
+ $filepath = rtrim( $this->app->_session['more_info']['convertGalleryImages']['file_location'], '/' ) . '/' . $filepath . '/' . $image['pictureid'] . '.picture';
+ }
+
+ $info = array(
+ 'image_id' => $image['pictureid'],
+ 'image_album_id' => $this->db->select( 'albumid', 'albumpicture', array( "pictureid=?", $image['pictureid'] ) )->first(),
+ 'image_member_id' => $image['userid'],
+ 'image_caption' => $image['caption'],
+ 'image_file_name' => $image['caption'] . '.' . $image['extension'],
+ 'image_date' => $this->db->select( 'dateline', 'albumpicture', array( "pictureid=?", $image['pictureid'] ) )->first(),
+ );
+
+ $libraryClass->convertGalleryImage( $info, $filepath, $filedata );
+
+ $libraryClass->setLastKeyValue( $image['pictureid'] );
+ }
+ }
+ else
+ {
+ $libraryClass::setKey( 'attachmentid' );
+
+ foreach( $this->fetch( 'attachment', 'attachmentid', array( "contenttypeid=?", static::$imageContentType ) ) AS $image )
+ {
+ try
+ {
+ $data = $this->db->select( '*', 'filedata', array( "filedataid=?", $image['filedataid'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $libraryClass->setLastKeyValue( $image['attachmentid'] );
+ continue;
+ }
+
+ $filedata = NULL;
+ $filepath = NULL;
+
+ if ( $this->app->_session['more_info']['convertGalleryImages']['file_location'] == 'database' )
+ {
+ $filedata = $data['filedata'];
+ }
+ else
+ {
+ $filepath = implode( '/', preg_split( '//', $data['userid'], -1, PREG_SPLIT_NO_EMPTY ) );
+ $filepath = rtrim( $this->app->_session['more_info']['convertGalleryImages']['file_location'], '/' ) . '/' . $filepath . '/' . $data['filedataid'] . '.attach';
+ }
+
+ $info = array(
+ 'image_id' => $image['attachmentid'],
+ 'image_album_id' => $image['contentid'],
+ 'image_member_id' => $image['userid'],
+ 'image_caption' => $image['caption'],
+ 'image_file_name' => $image['filename'],
+ 'image_date' => $image['dateline']
+ );
+
+ $libraryClass->convertGalleryImage( $info, $filepath, $filedata );
+
+ $libraryClass->setLastKeyValue( $image['attachmentid'] );
+ }
+ }
+ }
+
+ /**
+ * Convert gallery comments
+ *
+ * @return void
+ */
+ public function convertGalleryComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'commentid' );
+
+ foreach( $this->fetch( 'picturecomment', 'commentid' ) AS $comment )
+ {
+ switch( $comment['state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderation':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ /* For 3.x this is just the value stored for pictureid */
+ $image_id = $comment['pictureid'];
+
+ /* For 4.x though, we have to get the attachmentid based on the filedataid */
+ if ( static::$isLegacy === FALSE )
+ {
+ /* If VB tracked the attachmentid, just use it so we don't have to query */
+ if( $comment['sourceattachmentid'] )
+ {
+ $image_id = $comment['sourceattachmentid'];
+ }
+ else
+ {
+ try
+ {
+ $image_id = $this->db->select( 'attachmentid', 'attachment', array( array( 'filedataid=?', $comment['filedataid'] ) ) )->first();
+ }
+ catch( \Exception $e )
+ {
+ $image_id = 0;
+ }
+ }
+ }
+
+ $libraryClass->convertGalleryComment( array(
+ 'comment_id' => $comment['commentid'],
+ 'comment_text' => $comment['pagetext'],
+ 'comment_img_id' => $image_id,
+ 'comment_author_id' => $comment['postuserid'],
+ 'comment_author_name' => $comment['postusername'],
+ 'comment_post_date' => $comment['dateline'],
+ 'comment_approved' => $approved,
+ ) );
+
+ $libraryClass->setLastKeyValue( $comment['commentid'] );
+ }
+ }
+
+ /**
+ * Check if we can redirect the legacy URLs from this software to the new locations
+ *
+ * @return NULL|\IPS\Http\Url
+ */
+ public function checkRedirects()
+ {
+ $url = \IPS\Request::i()->url();
+
+ try
+ {
+ if( isset( \IPS\Request::i()->albumid ) )
+ {
+ $data = $this->app->getLink( \IPS\Request::i()->albumid, 'gallery_albums' );
+ $item = \IPS\gallery\Album::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'album.php' ) !== FALSE AND isset( \IPS\Request::i()->pictureid ) )
+ {
+ $data = $this->app->getLink( \IPS\Request::i()->pictureid, 'gallery_images' );
+ $item = \IPS\gallery\Image::load( $data );
+
+ if( $item->canView() )
+ {
+ return $item->url();
+ }
+ }
+ elseif( isset( \IPS\Request::i()->userid ) )
+ {
+ $data = $this->app->getLink( \IPS\Request::i()->userid, array( 'members', 'core_members' ) );
+ return \IPS\Member::load( $data )->url();
+ }
+ elseif( ( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'picture.php' ) !== FALSE AND isset( \IPS\Request::i()->pictureid ) ) OR
+ ( mb_strpos( $url->data[ \IPS\Http\Url::COMPONENT_PATH ], 'attachment.php' ) !== FALSE AND isset( \IPS\Request::i()->attachmentid ) ) )
+ {
+ try
+ {
+ $data = $this->app->getLink( \IPS\Request::i()->pictureid ?: \IPS\Request::i()->attachmentid, 'gallery_images' );
+ }
+ catch( \OutOfRangeException $e )
+ {
+ $data = \IPS\Request::i()->pictureid ?: \IPS\Request::i()->attachmentid;
+ }
+
+ return \IPS\File::get( 'gallery_Images', \IPS\gallery\Image::load( $data )->masked_file_name )->url;
+ }
+ }
+ catch( \Exception $e )
+ {
+ return NULL;
+ }
+
+ return NULL;
+ }
+}
\ No newline at end of file
diff --git a/applications/convert/sources/Software/Gallery/Xenforo.php b/applications/convert/sources/Software/Gallery/Xenforo.php
new file mode 100644
index 00000000..5a810f9d
--- /dev/null
+++ b/applications/convert/sources/Software/Gallery/Xenforo.php
@@ -0,0 +1,637 @@
+Invision Power Services, Inc.
+ * @copyright (c) 2001 - 2016 Invision Power Services, Inc.
+ * @package IPS Community Suite
+ * @subpackage Converter
+ * @since 21 Jan 2015
+ */
+
+namespace IPS\convert\Software\Gallery;
+
+/* To prevent PHP errors (extending class does not exist) revealing path */
+if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
+{
+ header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
+ exit;
+}
+
+class _Xenforo extends \IPS\convert\Software
+{
+ /**
+ * Software Name
+ *
+ * @return string
+ */
+ public static function softwareName()
+ {
+ /* Child classes must override this method */
+ return "XenForo Media Gallery";
+ }
+
+ /**
+ * Software Key
+ *
+ * @return string
+ */
+ public static function softwareKey()
+ {
+ /* Child classes must override this method */
+ return "xenforo";
+ }
+
+ /**
+ * Content we can convert from this software.
+ *
+ * @return array
+ */
+ public static function canConvert()
+ {
+ return array(
+ 'convertGalleryCategories' => array(
+ 'table' => 'xengallery_category',
+ 'where' => NULL
+ ),
+ 'convertGalleryAlbums' => array(
+ 'table' => 'xengallery_album',
+ 'where' => NULL,
+ ),
+ 'convertGalleryImages' => array(
+ 'table' => 'xengallery_media',
+ 'where' => NULL,
+ ),
+ 'convertGalleryComments' => array(
+ 'table' => 'xengallery_comment',
+ 'where' => NULL,
+ )
+ );
+ }
+
+ /**
+ * Uses Prefix
+ *
+ * @return bool
+ */
+ public static function usesPrefix()
+ {
+ return FALSE;
+ }
+
+ /**
+ * Requires Parent
+ *
+ * @return boolean
+ */
+ public static function requiresParent()
+ {
+ return TRUE;
+ }
+
+ /**
+ * Possible Parent Conversions
+ *
+ * @return array
+ */
+ public static function parents()
+ {
+ return array( 'core' => array( 'xenforo' ) );
+ }
+
+ /**
+ * List of conversion methods that require additional information
+ *
+ * @return array
+ */
+ public static function checkConf()
+ {
+ return array(
+ 'convertGalleryAlbums',
+ 'convertGalleryImages'
+ );
+ }
+
+ /**
+ * Get More Information
+ *
+ * @param string $method Conversion method
+ * @return array
+ */
+ public function getMoreInfo( $method )
+ {
+ $return = array();
+ switch( $method )
+ {
+ case 'convertGalleryAlbums':
+ $options = array();
+ foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'gallery_categories' ), 'IPS\gallery\Category' ) AS $category )
+ {
+ $options[$category->_id] = $category->_title;
+ }
+
+ $return['convertGalleryAlbums']['members_gallery_category'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Select',
+ 'field_default' => NULL,
+ 'field_required' => FALSE,
+ 'field_extra' => array(
+ 'options' => $options
+ ),
+ 'field_hint' => NULL,
+ );
+ break;
+ case 'convertGalleryImages':
+ $return['convertGalleryImages']['file_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_attach_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ $return['convertGalleryImages']['media_thumb_location'] = array(
+ 'field_class' => 'IPS\\Helpers\\Form\\Text',
+ 'field_default' => NULL,
+ 'field_required' => TRUE,
+ 'field_extra' => array(),
+ 'field_hint' => \IPS\Member::loggedIn()->language()->addToStack('convert_xf_gal_path'),
+ 'field_validation' => function( $value ) { if ( !@is_dir( $value ) ) { throw new \DomainException( 'path_invalid' ); } },
+ );
+ break;
+ }
+
+ return ( isset( $return[ $method ] ) ) ? $return[ $method ] : array();
+ }
+
+ /**
+ * Finish - Adds everything it needs to the queues and clears data store
+ *
+ * @return array Messages to display
+ */
+ public function finish()
+ {
+ /* Content Rebuilds */
+ \IPS\Task::queue( 'convert', 'RebuildGalleryImages', array( 'app' => $this->app->app_id ), 2, array( 'app' ) );
+ \IPS\Task::queue( 'core', 'RebuildItemCounts', array( 'class' => 'IPS\gallery\Image' ), 3, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Album', 'count' => 0 ), 4, array( 'class' ) );
+ \IPS\Task::queue( 'core', 'RebuildContainerCounts', array( 'class' => 'IPS\gallery\Category', 'count' => 0 ), 5, array( 'class' ) );
+
+ /* Caches */
+ \IPS\Task::queue( 'convert', 'RebuildTagCache', array( 'app' => $this->app->app_id, 'link' => 'gallery_images', 'class' => 'IPS\gallery\Image' ), 3, array( 'app', 'link', 'class' ) );
+
+ return array( "f_gallery_images_rebuild", "f_gallery_cat_recount", "f_gallery_album_recount", "f_gallery_image_recount", "f_image_tags_recount" );
+ }
+
+ /**
+ * Helper to fetch a xenforo phrase
+ *
+ * @param string $title Phrase
+ * @return string|null
+ */
+ protected function getPhrase( $title )
+ {
+ try
+ {
+ return $this->db->select( 'phrase_text', 'xf_phrase', array( "title=?", $title ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ return NULL;
+ }
+ }
+
+ /**
+ * Convert gallery categories
+ *
+ * @return void
+ */
+ public function convertGalleryCategories()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'category_id' );
+
+ foreach( $this->fetch( 'xengallery_category', 'category_id' ) AS $row )
+ {
+ $libraryClass->convertGalleryCategory( array(
+ 'category_id' => $row['category_id'],
+ 'category_name' => $row['category_title'],
+ 'category_desc' => $row['category_description'],
+ 'category_parent_id' => $row['parent_category_id'],
+ 'category_count_imgs' => $row['category_media_count'],
+ 'category_position' => $row['display_order']
+ ) );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xengallery_category_watch', array( "notify_on=? AND category_id=?", 'media', $row['category_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'gallery',
+ 'follow_area' => 'category',
+ 'follow_rel_id' => $row['category_id'],
+ 'follow_rel_id_type' => 'gallery_categories',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['send_alert'] OR $follow['send_email'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+ }
+ }
+
+ /**
+ * Convert gallery albums
+ *
+ * @return void
+ */
+ public function convertGalleryAlbums()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'album_id' );
+
+ foreach( $this->fetch( 'xengallery_album', 'album_id' ) AS $row )
+ {
+ try
+ {
+ $perm = $this->db->select( '*', 'xengallery_album_permission', array( "album_id=? AND permission=?", $row['album_id'], 'view' ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If the permission row is missing for some reason, err on the side of caution and set this album private */
+ $perm = array(
+ 'album_id' => $row['album_id'],
+ 'permission' => 'view',
+ 'access_type' => 'private',
+ 'share_users' => \serialize( array() ),
+ );
+ }
+
+ $socialgroup = NULL;
+ if ( $perm['access_type'] == 'shared' OR $perm['access_type'] == 'followed' )
+ {
+ switch( $perm['access_type'] )
+ {
+ /* Specific members */
+ case 'shared':
+ $users = \unserialize( $perm['share_users'] );
+ $socialgroup['members'] = array();
+
+ if ( count( $users ) )
+ {
+ foreach( $users AS $key => $user )
+ {
+ $socialgroup['members'][] = $user['shared_user_id'];
+ }
+ }
+ break;
+
+ /* Followers */
+ case 'followed':
+ $socialgroup['members'] = array();
+ foreach( $this->db->select( 'follow_user_id', 'xf_user_follow', array( "user_id=?", $row['album_user_id'] ) ) AS $user )
+ {
+ $socialgroup['members'][] = $user;
+ }
+ break;
+ }
+ }
+
+ switch( $perm['access_type'] )
+ {
+ case 'private':
+ case 'members': # we don't have an equivalent of "members only" but we don't want to just open everything up either, so set these private.
+ $type = 2;
+ break;
+
+ case 'public':
+ $type = 1;
+ break;
+
+ case 'followed':
+ case 'shared':
+ $type = 3;
+ break;
+ }
+
+ $info = array(
+ 'album_id' => $row['album_id'],
+ 'album_owner_id' => $row['album_user_id'],
+ 'album_description' => $row['album_description'],
+ 'album_name' => $row['album_title'],
+ 'album_type' => $type,
+ 'album_count_imgs' => $row['album_media_count'],
+ 'album_count_comments' => $row['album_comment_count'],
+ 'album_rating_aggregate' => $row['album_rating_avg'],
+ 'album_rating_count' => $row['album_rating_count'],
+ 'album_rating_total' => $row['album_rating_sum'],
+ );
+
+ $category = $this->app->_session['more_info']['convertGalleryAlbums']['members_gallery_category'];
+ if ( $category == 0 )
+ {
+ $category = NULL;
+ }
+
+ $libraryClass->convertGalleryAlbum( $info, $socialgroup, $category );
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xengallery_album_watch', array( "notify_on=? AND album_id=?", 'media', $row['album_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'gallery',
+ 'follow_area' => 'album',
+ 'follow_rel_id' => $row['album_id'],
+ 'follow_rel_id_type' => 'gallery_albums',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['send_alert'] OR $follow['send_email'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['album_id'] );
+ }
+ }
+
+ /**
+ * Convert gallery images
+ *
+ * @return void
+ */
+ public function convertGalleryImages()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'media_id' );
+
+ foreach( $this->fetch( 'xengallery_media', 'media_id' ) AS $row )
+ {
+ /* Exif */
+ $exif = array();
+ foreach( $this->db->select( 'exif_name, exif_value', 'xengallery_exif', array( "media_id=?", $row['media_id'] ) )->setKeyField( 'exif_name' )->setValueField( 'exif_value' ) AS $key => $value )
+ {
+ $exif[$key] = $value;
+ }
+
+ /* IP Address */
+ try
+ {
+ $ip = $this->db->select( '*', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+
+ /* Now the fun part... we need to do different things depending on the type of item this is. */
+ switch( $row['media_type'] )
+ {
+ /* Normal image upload */
+ case 'image_upload':
+ try
+ {
+ $file_data = $this->db->select( '*', 'xf_attachment', array( "xf_attachment.content_type=? AND xf_attachment.content_id=?", 'xengallery_media', $row['media_id'] ) )
+ ->join( 'xf_attachment_data', 'xf_attachment.data_id = xf_attachment_data.data_id' )
+ ->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ /* If the file data is missing, we can't do anything. */
+ $libraryClass->setLastKeyValue( $row['media_id'] );
+ continue 2;
+ }
+
+ $group = floor( $file_data['data_id'] / 1000 );
+ $path = rtrim( $this->app->_session['more_info']['convertGalleryImages']['file_location'], '/' ) . '/' . $group . '/' . $file_data['data_id'] . '-' . $file_data['file_hash'] . '.data';
+ $file_name = $file_data['filename'];
+ $description = $row['media_description'];
+
+ break;
+
+ /* Video Embed */
+ case 'video_embed':
+ $path = $this->app->_session['more_info']['convertGalleryImages']['media_thumb_location'];
+
+ /* We need to figure out what service we came from, and the video ID */
+ preg_match( '/\[media=(.*?)\](.*?)\[\/media\]/i', $row['media_tag'], $matches );
+ $service = $matches[1];
+ $video_id = $matches[2];
+
+ $file_name = $service . '_' . $video_id . '.jpg';
+ $path = rtrim( $path, '/' ) . '/' . $service . '/' . $file_name;
+
+ try
+ {
+ $em = \IPS\Text\Parser::embeddableMedia( $row['media_embed_url'] );
+ }
+ catch( \Exception $e )
+ {
+ /* If anything went wrong, back out */
+ $libraryClass->setlastKeyValue( $row['media_id'] );
+ continue 2;
+ }
+
+ $description = $em . " " . $row['media_description'];
+
+ break;
+ }
+
+ $info = array(
+ 'image_id' => $row['media_id'],
+ 'image_album_id' => $row['album_id'],
+ 'image_category_id' => $row['category_id'],
+ 'image_member_id' => $row['user_id'],
+ 'image_caption' => $row['media_title'],
+ 'image_views' => $row['media_view_count'],
+ 'image_comments' => $row['comment_count'],
+ 'image_ratings_total' => $row['rating_sum'],
+ 'image_ratings_count' => $row['rating_count'],
+ 'image_rating' => $row['rating_avg'],
+ 'image_date' => $row['media_date'],
+ 'image_updated' => $row['last_edit_date'],
+ 'image_last_comment' => $row['last_comment_date'],
+ 'image_metadata' => json_encode( $exif ),
+ 'image_ipaddress' => $ip,
+ 'image_file_name' => $file_name,
+ 'image_description' => $description
+ );
+
+ $libraryClass->convertGalleryImage( $info, $path );
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'xf_warning', array( "content_type=? AND content_id=?", 'xengallery_media', $row['media_id'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['warning_id'],
+ 'wl_member' => $warn['user_id'],
+ 'wl_moderator' => $warn['warning_user_id'],
+ 'wl_date' => $warn['warning_date'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['title'],
+ 'wl_note_mods' => $warn['notes'],
+ ) );
+ }
+
+ /* Reputation */
+ $likes = \unserialize( $row['like_users'] );
+ if ( is_array( $likes ) AND count( $likes ) )
+ {
+ foreach( $likes AS $like )
+ {
+ $libraryClass->convertReputation( array(
+ 'app' => 'gallery',
+ 'type' => 'image_id',
+ 'type_id' => $row['media_id'],
+ 'member_id' => $like['user_id'],
+ 'member_received' => $row['user_id'],
+ ) );
+ }
+ }
+
+ /* Follows */
+ foreach( $this->db->select( '*', 'xengallery_media_watch', array( "media_id=?", $row['media_id'] ) ) AS $follow )
+ {
+ $libraryClass->convertFollow( array(
+ 'follow_app' => 'gallery',
+ 'follow_area' => 'image',
+ 'follow_rel_id' => $row['media_id'],
+ 'follow_rel_id_type' => 'gallery_images',
+ 'follow_member_id' => $follow['user_id'],
+ 'follow_is_anon' => 0,
+ 'follow_added' => time(),
+ 'follow_notify_do' => 1,
+ 'follow_notify_meta' => '',
+ 'follow_notify_freq' => ( $follow['send_alert'] OR $follow['send_email'] ) ? 'immediate' : 'none',
+ 'follow_notify_sent' => 0,
+ 'follow_visible' => 1,
+ 'follow_index_id' => NULL
+ ) );
+ }
+
+ $tags = NULL;
+ /* Tags 1.0.x */
+ if( isset( $row['media_content_tag_cache'] ) )
+ {
+ $tags = \unserialize( $row['media_content_tag_cache'] );
+ }
+ /* Tags 1.1.x */
+ elseif( isset( $row['tags'] ) )
+ {
+ $tags = \unserialize( $row['tags'] );
+ }
+
+ if ( is_array( $tags ) and count( $tags ) )
+ {
+ foreach( $tags AS $k => $tag )
+ {
+ $libraryClass->convertTag( array(
+ 'tag_meta_app' => 'gallery',
+ 'tag_meta_area' => 'gallery',
+ 'tag_meta_parent_id' => ( $row['album_id'] ) ? $row['album_id'] : $row['category_id'],
+ 'tag_meta_id' => $row['media_id'],
+ 'tag_text' => isset( $tag['tag_clean'] ) ? $tag['tag_clean'] : $tag['tag'], // Select 1.0 or 1.1 version
+ 'tag_prefix' => 0,
+ 'tag_member_id' => $row['user_id']
+ ) );
+ }
+ }
+
+ $libraryClass->setLastKeyValue( $row['media_id'] );
+ }
+ }
+
+ /**
+ * Convert gallery comments
+ *
+ * @return void
+ */
+ public function convertGalleryComments()
+ {
+ $libraryClass = $this->getLibrary();
+
+ $libraryClass::setKey( 'comment_id' );
+
+ foreach( $this->fetch( 'xengallery_comment', 'comment_id', array( "content_type=?", 'media' ) ) AS $row )
+ {
+ /* IP Address */
+ try
+ {
+ $ip = $this->db->select( 'ip', 'xf_ip', array( "ip_id=?", $row['ip_id'] ) )->first();
+ }
+ catch( \UnderflowException $e )
+ {
+ $ip = '127.0.0.1';
+ }
+
+ /* Approved */
+ switch( $row['comment_state'] )
+ {
+ case 'visible':
+ $approved = 1;
+ break;
+
+ case 'moderated':
+ $approved = 0;
+ break;
+
+ case 'deleted':
+ $approved = -1;
+ break;
+ }
+
+ $libraryClass->convertGalleryComment( array(
+ 'comment_id' => $row['comment_id'],
+ 'comment_text' => $row['message'],
+ 'comment_img_id' => $row['content_id'],
+ 'comment_author_id' => $row['user_id'],
+ 'comment_author_name' => $row['username'],
+ 'comment_ip_address' => $ip,
+ 'comment_post_date' => $row['comment_date'],
+ 'comment_approved' => $approved,
+ ) );
+
+ /* Reputation */
+ $likes = \unserialize( $row['like_users'] );
+ if ( is_array( $likes ) AND count( $likes ) )
+ {
+ foreach( $likes AS $like )
+ {
+ $libraryClass->convertReputation( array(
+ 'app' => 'gallery',
+ 'type' => 'comment_id',
+ 'type_id' => $row['comment_id'],
+ 'member_id' => $like['user_id'],
+ 'member_received' => $row['user_id'],
+ ) );
+ }
+ }
+
+ /* Warnings */
+ foreach( $this->db->select( '*', 'xf_warning', array( "content_type=? AND content_id=?", 'xengallery_comment', $row['comment_id'] ) ) AS $warn )
+ {
+ $libraryClass->convertWarnLog( array(
+ 'wl_id' => $warn['warning_id'],
+ 'wl_member' => $warn['user_id'],
+ 'wl_moderator' => $warn['warning_user_id'],
+ 'wl_date' => $warn['warning_date'],
+ 'wl_points' => $warn['points'],
+ 'wl_note_member' => $warn['title'],
+ 'wl_note_mods' => $warn['notes'],
+ ) );
+ }
+
+ $libraryClass->setLastKeyValue( $row['comment_id'] );
+ }
+ }
+}
\ No newline at end of file
diff --git a/applications/chat/interface/sounds/index.html b/applications/convert/sources/index.html
similarity index 100%
rename from applications/chat/interface/sounds/index.html
rename to applications/convert/sources/index.html
diff --git a/applications/chat/modules/admin/chat/index.html b/applications/convert/tasks/index.html
similarity index 100%
rename from applications/chat/modules/admin/chat/index.html
rename to applications/convert/tasks/index.html
diff --git a/applications/core/data/acpsearch.json b/applications/core/data/acpsearch.json
index 0fa8f0c4..348a96c9 100644
--- a/applications/core/data/acpsearch.json
+++ b/applications/core/data/acpsearch.json
@@ -1,2693 +1,2042 @@
-{
- "app=core&module=applications&controller=api": {
- "lang_key": "menu__core_applications_api",
- "restriction": "api_manage",
- "keywords": [
- "api",
- "rest api",
- "апи"
- ]
- },
- "app=core&module=applications&controller=api&tab=apiKeys": {
- "lang_key": "api_keys",
- "restriction": "api_manage",
- "keywords": [
- "api keys",
- "ключи api"
- ]
- },
- "app=core&module=applications&controller=api&tab=apiLogs": {
- "lang_key": "api_logs",
- "restriction": "api_logs",
- "keywords": [
- "api logs",
- "журналы api",
- "логи api"
- ]
- },
- "app=core&module=applications&controller=api&tab=apiReference": {
- "lang_key": "api_reference",
- "restriction": "api_reference",
- "keywords": [
- "api documentation",
- "api reference",
- "документация api"
- ]
- },
- "app=core&module=applications&controller=applications": {
- "lang_key": "menu__core_applications_applications",
- "restriction": "app_manage",
- "keywords": [
- "applications",
- "install",
- "modules",
- "offline",
- "online",
- "приложения",
- "установить",
- "модули",
- "офлайн",
- "оффлайн",
- "в сети",
- "онлайн",
- "не в сети"
- ]
- },
- "app=core&module=applications&controller=enhancements": {
- "lang_key": "menu__core_applications_enhancements",
- "restriction": "enhancements_manage",
- "keywords": [
- "bulk mail",
- "email",
- "enhancements",
- "geo",
- "maps",
- "maxmind",
- "sendgrid",
- "spam",
- "гео",
- "расширения",
- "карты",
- "массовые рассылки",
- "спам",
- "письма"
- ]
- },
- "app=core&module=applications&controller=enhancements&do=edit&id=core_GoogleMaps&searchResult=form_googlemaps": {
- "lang_key": "googlemaps",
- "restriction": "enhancements_manage",
- "keywords": [
- "google",
- "maps",
- "гугл",
- "карты"
- ]
- },
- "app=core&module=applications&controller=enhancements&do=edit&id=core_Sparkpost": {
- "lang_key": "enhancements__core_Sparkpost",
- "restriction": "enhancements_manage",
- "keywords": [
- "bulk mail",
- "email",
- "sparkpost",
- "почта",
- "массовые рассылки",
- "массовая рассылка"
- ]
- },
- "app=core&module=applications&controller=enhancements&do=edit&id=core_Viglink&searchResult=form_viglink_account_type": {
- "lang_key": "viglink_account_type",
- "restriction": "enhancements_manage",
- "keywords": [
- "advertising",
- "viglink",
- "реклама"
- ]
- },
- "app=core&module=applications&controller=plugins": {
- "lang_key": "menu__core_applications_plugins",
- "restriction": "plugins_view",
- "keywords": [
- "add-ons",
- "addons",
- "hooks",
- "install",
- "mod",
- "modification",
- "plugins",
- "плагины",
- "дополнения",
- "хуки",
- "хук",
- "установить",
- "модификация",
- "мод"
- ]
- },
- "app=core&module=bulkmail&controller=bulkmail": {
- "lang_key": "menu__core_bulkmail_bulkmail",
- "restriction": "bulkmail_manage",
- "keywords": [
- "announce",
- "blast",
- "bulk",
- "bulk mail",
- "campaign",
- "email",
- "mail",
- "mass",
- "newsletter",
- "sendgrid",
- "анонс",
- "компания",
- "почта",
- "массовые рассылки",
- "новостные рассылки"
- ]
- },
- "app=core&module=customization&controller=emails": {
- "lang_key": "menu__core_customization_emails",
- "restriction": "emails_manage",
- "keywords": [
- "email content",
- "содержимое почты"
- ]
- },
- "app=core&module=customization&controller=themes": {
- "lang_key": "menu__core_customization_themes",
- "restriction": "theme_sets_manage",
- "keywords": [
- "design",
- "look",
- "skins",
- "themes",
- "дизайн",
- "скины",
- "темы",
- "оформление"
- ]
- },
- "app=core&module=customization&controller=themes&do=designersmode&searchResult=form_theme_designers_mode": {
- "lang_key": "theme_designers_mode",
- "restriction": "theme_templates_manage",
- "keywords": [
- "designers mode",
- "режим дизайнера",
- "режим дизайнеров"
- ]
- },
- "app=core&module=discovery&controller=rss": {
- "lang_key": "menu__core_discovery_rss",
- "restriction": null,
- "keywords": [
- "feed",
- "rss",
- "лента"
- "фид",
- "поток"
- ]
- },
- "app=core&module=discovery&controller=search": {
- "lang_key": "menu__core_discovery_search",
- "restriction": "search_manage",
- "keywords": [
- "rebuild search",
- "rebuild search index",
- "reindex",
- "reindex search",
- "reindex search results",
- "search",
- "search index",
- "search index rebuild",
- "searching",
- "перестроить поиск",
- "перестроить индекс",
- "перестроить поисковый индекс",
- "переиндексация",
- "поисковый индекс",
- "поиск"
- ]
- },
- "app=core&module=discovery&controller=search&searchResult=search_index_timeframe": {
- "lang_key": "search_index_timeframe",
- "restriction": "search_manage",
- "keywords": [
- "search",
- "search content",
- "search cutoff",
- "search limit",
- "search results",
- "поиск",
- "содержание поиска",
- "результаты поиска",
- "лимит поиска"
- ]
- },
- "app=core&module=editor&controller=emoticons": {
- "lang_key": "menu__core_editor_emoticons",
- "restriction": "emoticons_manage",
- "keywords": [
- "emoticons",
- "smileys",
- "smilies",
- "смайлы",
- "смайлики"
- ]
- },
- "app=core&module=editor&controller=settings": {
- "lang_key": "menu__core_editor_settings",
- "restriction": "settings_manage",
- "keywords": [
- "editor",
- "редактор"
- ]
- },
- "app=core&module=editor&controller=settings&searchResult=form_editor_paragraph_padding": {
- "lang_key": "editor_paragraph_padding",
- "restriction": "settings_manage",
- "keywords": [
- "enter",
- "paragraph",
- "return",
- "параграф",
- "возврат",
- ""
- ]
- },
- "app=core&module=editor&controller=settings&searchResult=form_editor_paste_behaviour": {
- "lang_key": "editor_paste_behaviour",
- "restriction": "settings_manage",
- "keywords": [
- "formatting",
- "paste",
- "posting",
- "remove",
- "форматирование",
- "вставить",
- "удалить"
- ]
- },
- "app=core&module=editor&controller=toolbar": {
- "lang_key": "editor_customization",
- "restriction": "toolbar_manage",
- "keywords": [
- "bbcode",
- "buttons",
- "comment",
- "editor",
- "mobile",
- "phone",
- "post",
- "responsive",
- "tablet",
- "toolbars",
- "wysiwyg",
- "бб-код",
- "кнопки",
- "комментарий",
- "редактор",
- "смартфон",
- "мобильник",
- "сообщение",
- "пост",
- "планшет",
- "отзывчивость",
- "тулбар",
- "инструменты"
- ]
- },
- "app=core&module=languages&controller=languages": {
- "lang_key": "menu__core_languages_languages",
- "restriction": null,
- "keywords": [
- "languages",
- "locales",
- "translate",
- "translations",
- "языки",
- "локали",
- "перевод",
- "перевести"
- ]
- },
- "app=core&module=members&controller=groups": {
- "lang_key": "menu__core_members_groups",
- "restriction": "groups_manage",
- "keywords": [
- "groups",
- "группы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=3_gbw_post_highlight": {
- "lang_key": "gbw_post_highlight",
- "restriction": "groups_manage",
- "keywords": [
- "highlight",
- "выделение",
- "выделить"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_attach": {
- "lang_key": "g_attach",
- "restriction": "groups_manage",
- "keywords": [
- "attachments",
- "uploads",
- "вложения",
- "прикрепления",
- "загрузки"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_avoid_flood": {
- "lang_key": "g_avoid_flood",
- "restriction": "groups_manage",
- "keywords": [
- "flood control",
- "post flood control",
- "posting flood control",
- "контроль флуда"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_avoid_q": {
- "lang_key": "g_avoid_q",
- "restriction": "groups_manage",
- "keywords": [
- "hidden",
- "hide",
- "hiding",
- "mod queue",
- "moderation",
- "queue",
- "очередь",
- "скрыто",
- "скрыть",
- "модерация",
- "очередь модерации"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_bypass_badwords": {
- "lang_key": "g_bypass_badwords",
- "restriction": "groups_manage",
- "keywords": [
- "bad words",
- "profanity",
- "swear words",
- "плохие слова",
- "мат",
- "фильтрация",
- "ругательства",
- "ругань",
- "ненормативная лексика"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_delete_own_posts": {
- "lang_key": "g_delete_own_posts",
- "restriction": "groups_manage",
- "keywords": [
- "delete",
- "deleting",
- "удалить",
- "удаление"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_edit_posts": {
- "lang_key": "g_edit_posts",
- "restriction": "groups_manage",
- "keywords": [
- "edit",
- "editing",
- "изменить",
- "изменение"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_edit_profile": {
- "lang_key": "g_edit_profile",
- "restriction": "groups_manage",
- "keywords": [
- "edit profile",
- "profiles",
- "профили",
- "изменить профиль"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_icon": {
- "lang_key": "g_icon",
- "restriction": "groups_manage",
- "keywords": [
- "group icon",
- "иконка группы",
- "изображение группы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_post_polls": {
- "lang_key": "g_post_polls",
- "restriction": "groups_manage",
- "keywords": [
- "polls",
- "опросы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_ppd_limit": {
- "lang_key": "g_ppd_limit",
- "restriction": "groups_manage",
- "keywords": [
- "content restriction",
- "max content",
- "max posts",
- "per day",
- "positing limit",
- "positing restriction",
- "ограничения контента",
- "максимум контента",
- "максимум сообщение",
- "лимит публикаций",
- "ограничений публикаций",
- "ограничение постинга"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_prefixsuffix": {
- "lang_key": "g_prefixsuffix",
- "restriction": "groups_manage",
- "keywords": [
- "group color",
- "group colour",
- "цвет группы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_rep_max_positive": {
- "lang_key": "g_rep_max_positive",
- "restriction": "groups_manage",
- "keywords": [
- "karma",
- "like",
- "reputation",
- "карма",
- "репутация",
- "лайк"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_topic_rate_setting": {
- "lang_key": "g_topic_rate_setting",
- "restriction": "groups_manage",
- "keywords": [
- "rate",
- "ratings",
- "рейтинг",
- "рейтинги"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_view_board": {
- "lang_key": "g_view_board",
- "restriction": "groups_manage",
- "keywords": [
- "access",
- "allowed to enter community",
- "allowed to enter site",
- "allowed to view community",
- "allowed to view site",
- "user access site",
- "доступ",
- "доступ для входа на сайт",
- "вход на сайт",
- "доступ пользователя на сайт"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_g_vote_polls": {
- "lang_key": "g_vote_polls",
- "restriction": "groups_manage",
- "keywords": [
- "polls",
- "опросы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_disable_prefixes": {
- "lang_key": "gbw_disable_prefixes",
- "restriction": "groups_manage",
- "keywords": [
- "prefixes",
- "tagging",
- "tags",
- "теги",
- "префиксы",
- "тегирование"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_disable_tagging": {
- "lang_key": "gbw_disable_tagging",
- "restriction": "groups_manage",
- "keywords": [
- "tagging",
- "tags",
- "теги",
- "тегирование"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_no_report": {
- "lang_key": "gbw_no_report",
- "restriction": "groups_manage",
- "keywords": [
- "report",
- "reporting",
- "пожаловаться",
- "жалоба"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_no_status_import": {
- "lang_key": "gbw_no_status_import",
- "restriction": "groups_manage",
- "keywords": [
- "facebook status",
- "profile status",
- "status updates",
- "statuses",
- "twitter status",
- "статус профиля",
- "статусы",
- "статус в фейсбук",
- "статус в твиттер"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_no_status_update": {
- "lang_key": "gbw_no_status_update",
- "restriction": "groups_manage",
- "keywords": [
- "profile status",
- "status updates",
- "statuses",
- "статусы",
- "статусы профиля",
- "статусы в профиле",
- "обновление статуса",
- "обновления статусов"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_post_highlight": {
- "lang_key": "gbw_post_highlight",
- "restriction": "groups_manage",
- "keywords": [
- "highlight",
- "highlight post",
- "replies",
- "ответы",
- "выделение",
- "выделение сообщений",
- "выделение постов"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_soft_delete_own": {
- "lang_key": "gbw_soft_delete_own",
- "restriction": "groups_manage",
- "keywords": [
- "hide",
- "hiding",
- "скрытие",
- "скрыть"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=4_gbw_view_reps": {
- "lang_key": "gbw_view_reps",
- "restriction": "groups_manage",
- "keywords": [
- "karma",
- "like",
- "reputation",
- "карма",
- "лайк",
- "репутация"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_access_offline": {
- "lang_key": "g_access_offline",
- "restriction": "groups_manage",
- "keywords": [
- "offline access",
- "site offline",
- "закрытый доступ",
- "оффлайн доступ",
- "сайт выключен",
- "сайт отключен"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_append_edit": {
- "lang_key": "g_append_edit",
- "restriction": "groups_manage",
- "keywords": [
- "edit",
- "silent edit",
- "изменить",
- "изменить без причины",
- "изменить без указания причины"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_attach_max": {
- "lang_key": "g_attach_max",
- "restriction": "groups_manage",
- "keywords": [
- "attachments",
- "storage",
- "uploads",
- "вложения",
- "хранение",
- "загрузки"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_attach_per_post": {
- "lang_key": "g_attach_per_post",
- "restriction": "groups_manage",
- "keywords": [
- "upload per article",
- "upload per blog",
- "upload per content item",
- "upload per post",
- "upload per topic",
- "вложений в статье",
- "вложений в блоге",
- "вложений в сообщении",
- "вложений в теме"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_can_msg_attach": {
- "lang_key": "g_can_msg_attach",
- "restriction": "groups_manage",
- "keywords": [
- "messenger attachments",
- "messenger images",
- "messenger upload",
- "pm attachments",
- "pm images",
- "pm upload",
- "вложения в беседы",
- "вложение в личные сообщения",
- "прикрепления"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_dohtml": {
- "lang_key": "g_dohtml",
- "restriction": "groups_manage",
- "keywords": [
- "html posting",
- "post html",
- "html сообщение"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_edit_cutoff": {
- "lang_key": "g_edit_cutoff",
- "restriction": "groups_manage",
- "keywords": [
- "edit restriction",
- "edit time",
- "edit timing",
- "ограничение изменения",
- "время изменения",
- "время редактирования"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_hide_online_list": {
- "lang_key": "g_hide_online_list",
- "restriction": "groups_manage",
- "keywords": [
- "online list",
- "show user list",
- "show user online",
- "список онлайн",
- "онлайн список",
- "показать список пользователей",
- "список онлайн пользователей"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_max_bgimg_upload": {
- "lang_key": "g_max_bgimg_upload",
- "restriction": "groups_manage",
- "keywords": [
- "cover photo size",
- "profile cover photo size",
- "размер фото обложки",
- "фото обложки"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_max_messages": {
- "lang_key": "g_max_messages",
- "restriction": "groups_manage",
- "keywords": [
- "messenger storage",
- "personal messenger storage",
- "pm storage",
- "pms",
- "лс",
- "количество личных сообщение",
- "сохраняемые личные сообщения"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_photo_max_vars_size": {
- "lang_key": "g_photo_max_vars_size",
- "restriction": "groups_manage",
- "keywords": [
- "photo size",
- "photos",
- "profile photo size",
- "размер фото",
- "фото",
- "размер фото профиля"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_photo_max_vars_wh": {
- "lang_key": "g_photo_max_vars_wh",
- "restriction": "groups_manage",
- "keywords": [
- "photo size",
- "photos",
- "profile photo size",
- "размер фото",
- "фото",
- "размер фото профиля"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_pm_perday": {
- "lang_key": "g_pm_perday",
- "restriction": "groups_manage",
- "keywords": [
- "new pms",
- "personal messages",
- "personal messenger",
- "pm limit",
- "pms",
- "лс",
- "новые лс",
- "личные сообщения",
- "лимит лс",
- "беседы"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_ppd_unit": {
- "lang_key": "g_ppd_unit",
- "restriction": "groups_manage",
- "keywords": [
- "positing limit",
- "лимит постинга"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_search_flood": {
- "lang_key": "g_search_flood",
- "restriction": "groups_manage",
- "keywords": [
- "search flood control",
- "поисковый флуд контроль",
- "контроль флуда"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_sig_max_image_size": {
- "lang_key": "g_sig_max_image_size",
- "restriction": "groups_manage",
- "keywords": [
- "signature images",
- "signatures",
- "подписи",
- "изображения подписи"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_sig_max_images": {
- "lang_key": "g_sig_max_images",
- "restriction": "groups_manage",
- "keywords": [
- "signature images",
- "signatures",
- "подписи",
- "изображения подписи"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_sig_max_lines": {
- "lang_key": "g_sig_max_lines",
- "restriction": "groups_manage",
- "keywords": [
- "signature length",
- "signature size",
- "signature text",
- "signatures",
- "подписи",
- "длина подписи",
- "размер подписи",
- "текст подписи"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_sig_max_urls": {
- "lang_key": "g_sig_max_urls",
- "restriction": "groups_manage",
- "keywords": [
- "signature links",
- "signatures",
- "ссылки в подписи",
- "подпись",
- "подписи"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=g_use_signatures": {
- "lang_key": "g_use_signatures",
- "restriction": "groups_manage",
- "keywords": [
- "signatures",
- "подписи",
- "подпись"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=gbw_allow_upload_bgimage": {
- "lang_key": "gbw_allow_upload_bgimage",
- "restriction": "groups_manage",
- "keywords": [
- "cover photo",
- "profile cover photo",
- "фото обложки",
- "обложка профиля",
- "фотография обложки"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=gbw_cannot_be_ignored": {
- "lang_key": "can_be_ignored",
- "restriction": "groups_manage",
- "keywords": [
- "ignore",
- "ignored users",
- "игнор",
- "игнорировать пользователя"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=gbw_delete_attachments": {
- "lang_key": "gbw_delete_attachments",
- "restriction": "groups_manage",
- "keywords": [
- "attachments",
- "delete attachments",
- "uploads",
- "вложения",
- "удалить вложение",
- "загрузки",
- "удвлить вложения"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=group_promotion": {
- "lang_key": "group_promotion",
- "restriction": "groups_manage",
- "keywords": [
- "group promote",
- "group promotion",
- "group upgrade",
- "member promote",
- "member promotion",
- "member upgrade",
- "promote",
- "promotion",
- "продвинуть группу",
- "продвижение группы",
- "продвижение",
- "обновить группу"
- ]
- },
- "app=core&module=members&controller=groups&searchResult=group_username": {
- "lang_key": "group_username",
- "restriction": "groups_manage",
- "keywords": [
- "display name changes",
- "name changes",
- "user name changes",
- "username changes",
- "имя пользователя",
- "отображаемое имя",
- "изменить имя"
- ]
- },
- "app=core&module=members&controller=ip": {
- "lang_key": "menu__core_members_ip",
- "restriction": "membertools_ip",
- "keywords": [
- "address",
- "IP",
- "lookup",
- "адрес",
- "ИП"
- ]
- },
- "app=core&module=members&controller=members": {
- "lang_key": "menu__core_members_members",
- "restriction": null,
- "keywords": [
- "accounts",
- "administrators",
- "banned",
- "locked",
- "members",
- "spammers",
- "validating",
- "аккаунт",
- "администраторы",
- "заблокированные",
- "забаненные",
- "пользователи",
- "спамеры",
- "валидация",
- "ожидающие одобрения"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_allow_admin_mails": {
- "lang_key": "allow_admin_mails",
- "restriction": null,
- "keywords": [
- "Email",
- "Send Email",
- "Send News",
- "письмо",
- "отправить письмо",
- "отправить новость"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_auto_track": {
- "lang_key": "auto_track",
- "restriction": null,
- "keywords": [
- "Auto",
- "Automatically",
- "Content",
- "Follow",
- "авто",
- "автоматически",
- "контент",
- "подписка",
- "подписаться"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_group": {
- "lang_key": "group",
- "restriction": null,
- "keywords": [
- "Group",
- "группа"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_ips_address_mail": {
- "lang_key": "ips_address_mail",
- "restriction": null,
- "keywords": [
- "Email Address",
- "адрес почты",
- "email адоес"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_ips_member_name": {
- "lang_key": "ips_member_name",
- "restriction": null,
- "keywords": [
- "Display Name"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_member_content_items": {
- "lang_key": "member_content_items",
- "restriction": null,
- "keywords": [
- "Content Item",
- "Recount",
- "количество контента",
- "число контента",
- "пересчёт",
- "пересчитать"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_member_reputation": {
- "lang_key": "member_reputation",
- "restriction": null,
- "keywords": [
- "Recount",
- "Reputation Level",
- "пересчитать репутацию",
- "пересчёт",
- "пересчитать"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_member_title": {
- "lang_key": "member_title",
- "restriction": null,
- "keywords": [
- "Member Title",
- "заголовок пользователя",
- "статус пользователя",
- "титул пользователя"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_member_warnings": {
- "lang_key": "member_warnings",
- "restriction": null,
- "keywords": [
- "Warning Points",
- "баллы предупреждения",
- "баллы предупреждений"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_password": {
- "lang_key": "password",
- "restriction": null,
- "keywords": [
- "Change Password",
- "Password",
- "пароль",
- "изменить пароль"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_secondary_groups": {
- "lang_key": "secondary_groups",
- "restriction": null,
- "keywords": [
- "Secondary Groups",
- "вторичная группа",
- "вторичные группы"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_skin": {
- "lang_key": "skin",
- "restriction": null,
- "keywords": [
- "Theme",
- "тема"
- ]
- },
- "app=core&module=members&controller=members&searchResult=form_view_sigs": {
- "lang_key": "view_sigs",
- "restriction": null,
- "keywords": [
- "Signatures",
- "View",
- "подписи",
- "просмотр"
- ]
- },
- "app=core&module=membersettings&controller=ban": {
- "lang_key": "menu__core_membersettings_ban",
- "restriction": "ban_manage",
- "keywords": [
- "banned",
- "bans",
- "email",
- "ip address",
- "register",
- "username",
- "забаненные",
- "бан",
- "почта",
- "регистрация",
- "ip адрес"
- ]
- },
- "app=core&module=membersettings&controller=notifications": {
- "lang_key": "menu__core_membersettings_notifications",
- "restriction": "notifications_manage",
- "keywords": [
- "notifications",
- "уведомления"
- ]
- },
- "app=core&module=membersettings&controller=notifications&do=settings&searchResult=auto_follow_new_content": {
- "lang_key": "auto_follow_new_content",
- "restriction": "notifications_manage",
- "keywords": [
- "auto",
- "automatically",
- "content",
- "follow",
- "авто",
- "подписка",
- "контент",
- "подписаться",
- "автоматически"
- ]
- },
- "app=core&module=membersettings&controller=notifications&do=settings&searchResult=auto_follow_replied_to": {
- "lang_key": "auto_follow_replied_to",
- "restriction": "notifications_manage",
- "keywords": [
- "auto",
- "automatically",
- "content",
- "follow",
- "авто",
- "подписка",
- "контент",
- "подписаться",
- "автоматически"
- ]
- },
- "app=core&module=membersettings&controller=profiles": {
- "lang_key": "menu__core_membersettings_profiles",
- "restriction": "profiles_manage",
- "keywords": [
- "about me",
- "custom profile fields",
- "обо мне",
- "поля профиля"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=fields": {
- "lang_key": "profile_fields",
- "restriction": "profiles_manage",
- "keywords": [
- "custom fields",
- "custom profile",
- "custom profile fields",
- "edit fields",
- "edit profile fields",
- "profile fields",
- "поля профиля",
- "дополнительные поля",
- "изменить поле"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_allow_gravatars": {
- "lang_key": "allow_gravatars",
- "restriction": "profiles_manage",
- "keywords": [
- "gravatar",
- "граватар"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_profile_comments": {
- "lang_key": "profile_comments",
- "restriction": "profiles_manage",
- "keywords": [
- "profile comments",
- "profile statuses",
- "комментарии профиля",
- "статусы профиля"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_signatures_enabled": {
- "lang_key": "signatures_enabled",
- "restriction": "profiles_manage",
- "keywords": [
- "signatures",
- "подписи",
- "подпись"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_username_characters": {
- "lang_key": "username_characters",
- "restriction": "profiles_manage",
- "keywords": [
- "username characters",
- "символы имени"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=profile_comment_approval": {
- "lang_key": "profile_comment_approval",
- "restriction": "profiles_manage",
- "keywords": [
- "profile comments",
- "profile statuses",
- "комментарии профиля",
- "статусы профиля"
- ]
- },
- "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=user_name_length": {
- "lang_key": "user_name_length",
- "restriction": "profiles_manage",
- "keywords": [
- "display name length",
- "display name size",
- "maximum display name length",
- "maximum display name size",
- "maximum username length",
- "maximum username size",
- "minimum display name length",
- "minimum display name size",
- "minimum username length",
- "minimum username size",
- "username length",
- "username size",
- "длина имени пользователя",
- "размер имени пользователя",
- "максимальная длина"
- ]
- },
- "app=core&module=membersettings&controller=ranks": {
- "lang_key": "menu__core_membersettings_ranks",
- "restriction": "ranks_manage",
- "keywords": [
- "badges",
- "ranks",
- "titles",
- "звания",
- "ранги",
- "значки",
- "бейджи"
- ]
- },
- "app=core&module=membersettings&controller=registration": {
- "lang_key": "menu__core_membersettings_registration",
- "restriction": "registration_manage",
- "keywords": [
- "approve",
- "confirmation",
- "coppa",
- "registration",
- "validate",
- "validation",
- "одобирть",
- "регистрация",
- "валидация"
- ]
- },
- "app=core&module=membersettings&controller=registration&searchResult=form_allow_reg": {
- "lang_key": "allow_reg",
- "restriction": "registration_manage",
- "keywords": [
- "allow registrations",
- "new registrations",
- "registration",
- "registrations",
- "разрешить регистрацию",
- "регистрация",
- "регистрации"
- ]
- },
- "app=core&module=membersettings&controller=registration&searchResult=new_reg_notify": {
- "lang_key": "new_reg_notify",
- "restriction": "registration_manage",
- "keywords": [
- "email on",
- "email on new registration",
- "new registrations",
- "notify",
- "notify new registration",
- "registrations",
- "новые регистрации",
- "уведомление",
- "регистрации"
- ]
- },
- "app=core&module=membersettings&controller=registration&searchResult=use_coppa": {
- "lang_key": "use_coppa",
- "restriction": "registration_manage",
- "keywords": [
- "coppa"
- ]
- },
- "app=core&module=membersettings&controller=reputation": {
- "lang_key": "menu__core_membersettings_reputation",
- "restriction": "reps_manage",
- "keywords": [
- "karma",
- "likes",
- "reputation",
- "карма",
- "лайк",
- "репутация"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=form_reputation_enabled": {
- "lang_key": "reputation_enabled",
- "restriction": "reps_manage",
- "keywords": [
- "Likes",
- "negative reputation",
- "positive reputation",
- "Reputation",
- "репутация",
- "лайк",
- "позитивная репутация",
- "положительная репутация"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_can_self_vote": {
- "lang_key": "reputation_can_self_vote",
- "restriction": "reps_settings",
- "keywords": [
- "like own",
- "like self",
- "own",
- "rep self",
- "reputation self",
- "self",
- "self reputation",
- "собственная",
- "лайкать себя",
- "репутация себе"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_highlight": {
- "lang_key": "reputation_highlight",
- "restriction": "reps_manage",
- "keywords": [
- "popular",
- "популярно"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_leaderboard_default_tab": {
- "lang_key": "reputation_leaderboard_default_tab",
- "restriction": "reps_manage",
- "keywords": [
- "leaderboard",
- "member list",
- "лидеры",
- "доска почёта",
- "доска лидеров",
- "список пользователей"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_leaderboard_on": {
- "lang_key": "reputation_leaderboard_on",
- "restriction": "reps_manage",
- "keywords": [
- "leaderboard",
- "reputation",
- "доска почёта",
- "репутация",
- "лидеры"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_max_members": {
- "lang_key": "reputation_max_members",
- "restriction": "reps_manage",
- "keywords": [
- "top members",
- "топ пользователей",
- "популярные пользователи"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_protected_groups": {
- "lang_key": "reputation_protected_groups",
- "restriction": "reps_settings",
- "keywords": [
- "no likes",
- "no reputation",
- "protected",
- "нет лайков",
- "защищённые",
- "нет репутации",
- "без репутации"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_content": {
- "lang_key": "reputation_show_content",
- "restriction": "reps_settings",
- "keywords": [
- "show likes",
- "show reputation",
- "показать лайки",
- "показать репутацию"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_days_won_trophy": {
- "lang_key": "reputation_show_days_won_trophy",
- "restriction": "reps_manage",
- "keywords": [
- "days won",
- "дней выйграл",
- "выйгрыш"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_profile": {
- "lang_key": "reputation_show_profile",
- "restriction": "reps_manage",
- "keywords": [
- "show likes",
- "show reputation",
- "показать лайки",
- "показать репутацию"
- ]
- },
- "app=core&module=membersettings&controller=reputation&searchResult=reputation_timezone": {
- "lang_key": "reputation_timezone",
- "restriction": "reps_manage",
- "keywords": [
- "leaderboard",
- "timezone",
- "лидеры",
- "доска почёта",
- "доска лидеров",
- "временная зона",
- "часовой пояс"
- ]
- },
- "app=core&module=membersettings&controller=spam": {
- "lang_key": "menu__core_membersettings_spam",
- "restriction": "spam_manage",
- "keywords": [
- "spam",
- "spammers",
- "спам",
- "спамер",
- "спамеры"
- ]
- },
- "app=core&module=membersettings&controller=spam&do=serviceLogs": {
- "lang_key": "spamlogs",
- "restriction": "spam_service_log",
- "keywords": [
- "spam logs",
- "журналы спама",
- "логи спама"
- ]
- },
- "app=core&module=membersettings&controller=spam&searchResult=bot_antispam_type": {
- "lang_key": "bot_antispam_type",
- "restriction": "spam_manage",
- "keywords": [
- "bot prevention",
- "CAPTCHA",
- "security question",
- "капча",
- "антиспам",
- "безопасность"
- ]
- },
- "app=core&module=membersettings&controller=spam&searchResult=guest_captcha": {
- "lang_key": "guest_captcha_search_target",
- "restriction": "spam_manage",
- "keywords": [
- "delete guest content",
- "delete guest posts",
- "guest content",
- "guest posts",
- "сообщения гостя",
- "гостевые сообщения",
- "удалить сообщения гостя"
- ]
- },
- "app=core&module=membersettings&controller=spam&tab=captcha": {
- "lang_key": "spamprevention_captcha",
- "restriction": "spam_manage",
- "keywords": [
- "captcha",
- "keycaptcha",
- "recaptcha",
- "капча"
- ]
- },
- "app=core&module=membersettings&controller=spam&tab=flagging": {
- "lang_key": "spamprevention_flagging",
- "restriction": "spam_manage",
- "keywords": [
- "flagging",
- "флаг",
- "отметка",
- "пометка"
- ]
- },
- "app=core&module=membersettings&controller=spam&tab=qanda": {
- "lang_key": "qanda_settings",
- "restriction": "qanda_manage",
- "keywords": [
- "answers",
- "challenge",
- "question",
- "registration",
- "ответы",
- "вопросы",
- "регистрация"
- ]
- },
- "app=core&module=membersettings&controller=spam&tab=service": {
- "lang_key": "enhancements__core_SpamMonitoring",
- "restriction": "spam_manage",
- "keywords": [
- "ips spam monitor",
- "ips spam monitoring",
- "ips spam service",
- "мониторинг спама"
- ]
- },
- "app=core&module=membersettings&controller=warnings": {
- "lang_key": "menu__core_membersettings_warnings",
- "restriction": "warn_settings",
- "keywords": [
- "acknowledge",
- "actions",
- "reasons",
- "warnings",
- "причины",
- "предупреждения",
- "действия"
- ]
- },
- "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=form_warn_on": {
- "lang_key": "warn_on",
- "restriction": "warn_settings",
- "keywords": [
- "warning",
- "warning setting",
- "warning system",
- "предупреждение",
- "настройки предупреждений",
- "система предупреждений"
- ]
- },
- "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warn_protected": {
- "lang_key": "warn_protected",
- "restriction": "warn_settings",
- "keywords": [
- "no warnings",
- "protected",
- "защищены",
- "без предупреждений"
- ]
- },
- "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warn_show_own": {
- "lang_key": "warn_show_own",
- "restriction": "warn_settings",
- "keywords": [
- "own",
- "себя",
- "самому себе"
- ]
- },
- "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warnings_acknowledge": {
- "lang_key": "warnings_acknowledge",
- "restriction": "warn_settings",
- "keywords": [
- "acknowledge",
- "признать",
- "согласиться"
- ]
- },
- "app=core&module=overview&controller=files": {
- "lang_key": "menu__core_overview_files",
- "restriction": "files_view",
- "keywords": [
- "attachments",
- "files",
- "storage",
- "uploads",
- "вложения",
- "файлы",
- "хренение",
- "загрузки"
- ]
- },
- "app=core&module=overview&controller=files&do=imagesettings": {
- "lang_key": "image_settings",
- "restriction": "files_view",
- "keywords": [
- "gd",
- "imagemagick"
- ]
- },
- "app=core&module=overview&controller=files&do=imagesettings&searchResult=form_image_jpg_quality": {
- "lang_key": "image_jpg_quality",
- "restriction": "files_view",
- "keywords": [
- "jpeg",
- "jpg"
- ]
- },
- "app=core&module=overview&controller=files&do=imagesettings&searchResult=image_png_quality_gd": {
- "lang_key": "image_png_quality_gd",
- "restriction": "files_view",
- "keywords": [
- "png",
- "пнг"
- ]
- },
- "app=core&module=overview&controller=files&do=settings": {
- "lang_key": "menu__core_overview_files",
- "restriction": "files_view",
- "keywords": [
- "amazon",
- "files",
- "ftp",
- "photos",
- "storage",
- "uploads",
- "амазон",
- "файлы",
- "фтп",
- "фото",
- "вложения",
- "хранение",
- "загрузки"
- ]
- },
- "app=core&module=overview&controller=security": {
- "lang_key": "menu__core_overview_security",
- "restriction": "security_manage",
- "keywords": [
- "acp",
- "admin",
- "conf",
- "hack",
- "htaccess",
- "http",
- "security",
- "акп",
- "админка",
- "админцентре",
- "конфиг",
- "хак",
- "безопасность"
- ]
- },
- "app=core&module=overview&controller=security&do=settings&searchResult=form_match_ipaddress": {
- "lang_key": "match_ipaddress",
- "restriction": "security_manage",
- "keywords": [
- "ip address",
- "session",
- "ип адрес",
- "ip адрес",
- "сессия",
- "сесия"
- ]
- },
- "app=core&module=overview&controller=security&do=settings&searchResult=form_security_remove_acp_link": {
- "lang_key": "security_remove_acp_link",
- "restriction": "security_manage",
- "keywords": [
- "acp",
- "admin cp",
- "admincp",
- "акп",
- "админка",
- "админцентр"
- ]
- },
- "app=core&module=overview&controller=security&do=settings&searchResult=form_xforward_matching": {
- "lang_key": "xforward_matching",
- "restriction": "security_manage",
- "keywords": [
- "http_x_forwarded_for",
- "ip address",
- "proxies",
- "proxy",
- "прокси",
- "ип адрес",
- "ip адрес"
- ]
- },
- "app=core&module=overview&controller=security&do=settings&searchResult=password_strength_meter": {
- "lang_key": "password_strength_meter",
- "restriction": "security_manage",
- "keywords": [
- "meter",
- "password",
- "strength",
- "метр",
- "сложность",
- "строгий",
- "сложный",
- "пароль"
- ]
- },
- "app=core&module=promotion&controller=advertisements": {
- "lang_key": "menu__core_promotion_advertisements",
- "restriction": "advertisements_manage",
- "keywords": [
- "ads",
- "adsense",
- "advertisements",
- "adverts",
- "adwords",
- "google",
- "гугл",
- "реклама"
- ]
- },
- "app=core&module=promotion&controller=analytics": {
- "lang_key": "ipbseo_ga_enabled",
- "restriction": "analytics_manage",
- "keywords": [
- "analytics",
- "ga",
- "google",
- "google analytics",
- "omniture",
- "piwik",
- "tracking",
- "аналитика",
- "гугл"
- ]
- },
- "app=core&module=promotion&controller=seo": {
- "lang_key": "menu__core_promotion_seo",
- "restriction": "seo_manage",
- "keywords": [
- "analytics",
- "furl",
- "search",
- "search engine optimization",
- "seo",
- "аналитика",
- "чпу",
- "фурл",
- "сео",
- "оптимизация",
- "поисковая оптимизация"
- ]
- },
- "app=core&module=promotion&controller=seo&tab=metatags": {
- "lang_key": "seo_tab_metatags",
- "restriction": "seo_manage",
- "keywords": [
- "description",
- "keywords",
- "meta tags",
- "robots",
- "описание",
- "ключевые слова",
- "мета теги",
- "роботс"
- ]
- },
- "app=core&module=promotion&controller=seo&tab=sitemap": {
- "lang_key": "seo_tab_sitemap",
- "restriction": "seo_manage",
- "keywords": [
- "google",
- "priority",
- "site map",
- "sitemap",
- "xml",
- "гугл",
- "карта сайта",
- "приоритет"
- ]
- },
- "app=core&module=promotion&controller=seo&tab=urls": {
- "lang_key": "seo_tab_furls",
- "restriction": "seo_manage",
- "keywords": [
- "friendly url",
- "furls",
- "htaccess",
- "mod_rewrite",
- "urls",
- "чпу",
- "фурл"
- ]
- },
- "app=core&module=promotion&controller=sharelinks": {
- "lang_key": "menu__core_promotion_sharelinks",
- "restriction": "sharelinks_manage",
- "keywords": [
- "delicious",
- "digg",
- "facebook",
- "google",
- "linkedin",
- "reddit",
- "share links",
- "stumbleupon",
- "twitter",
- "твиттер",
- "поделиться",
- "социальные сети",
- "гугл",
- "фейсбук"
- ]
- },
- "app=core&module=settings&controller=advanced&do=tasks": {
- "lang_key": "task_manager",
- "restriction": null,
- "keywords": [
- "cron",
- "cronjob",
- "task",
- "tasklog",
- "tasks",
- "задачи",
- "задача",
- "крон"
- ]
- },
- "app=core&module=settings&controller=advanced&searchResult=form_auto_polling_enabled": {
- "lang_key": "auto_polling_enabled",
- "restriction": "advanced_manage",
- "keywords": [
- "auto",
- "automatic",
- "polling",
- "авто",
- "автоматически",
- "голосование"
- ]
- },
- "app=core&module=settings&controller=advanced&tab=datastore": {
- "lang_key": "data_store",
- "restriction": "datastore",
- "keywords": [
- "storage",
- "хранение"
- ]
- },
- "app=core&module=settings&controller=advanced&tab=datastore&searchResult=cache_guest_page": {
- "lang_key": "cache_guest_page",
- "restriction": "advanced_manage_server",
- "keywords": [
- "cache",
- "guest cache",
- "output cache",
- "кеш",
- "кэш",
- "гостевой кэш"
- ]
- },
- "app=core&module=settings&controller=advanced&tab=datastore&searchResult=cache_method": {
- "lang_key": "cache_method",
- "restriction": "datastore",
- "keywords": [
- "apc",
- "memcached",
- "redis",
- "wincache",
- "xcache"
- ]
- },
- "app=core&module=settings&controller=advanced&tab=furl": {
- "lang_key": "furls_customize",
- "restriction": "advanced_manage_furls",
- "keywords": [
- "friendly",
- "furl",
- "seo",
- "чпу",
- "сео",
- "фурл"
- ]
- },
- "app=core&module=settings&controller=advanced&tab=settings&searchResult=task_use_cron": {
- "lang_key": "task_use_cron",
- "restriction": "advanced_manage_server",
- "keywords": [
- "cron job",
- "cronjob",
- "tasks",
- "задача",
- "крон"
- ]
- },
- "app=core&module=settings&controller=contactus": {
- "lang_key": "menu__core_settings_contactus",
- "restriction": "contactus_manage",
- "keywords": [
- "contact us",
- "написать нам",
- "обратная связь"
- ]
- },
- "app=core&module=settings&controller=email": {
- "lang_key": "menu__core_settings_email",
- "restriction": "email_manage",
- "keywords": [
- "address",
- "email",
- "email logo",
- "mail",
- "smtp",
- "почта",
- "адрес",
- "лого почты"
- ]
- },
- "app=core&module=settings&controller=email&do=errorLog": {
- "lang_key": "emailerrorlogs",
- "restriction": "email_errorlog",
- "keywords": [
- "email error logs",
- "email logs",
- "logs",
- "журналы",
- "логи",
- "журнал ошибок",
- "лог ошибок"
- ]
- },
- "app=core&module=settings&controller=email&searchResult=settings_form_email_color": {
- "lang_key": "email_color",
- "restriction": "email_manage",
- "keywords": [
- "email color",
- "email colour",
- "цвет почты",
- "цвет email"
- ]
- },
- "app=core&module=settings&controller=email&searchResult=settings_form_email_in": {
- "lang_key": "email_in",
- "restriction": "email_manage",
- "keywords": [
- "contact email",
- "contact us email",
- "incoming email",
- "почта обратной связи",
- "контактная почта",
- "входящая почта"
- ]
- },
- "app=core&module=settings&controller=email&searchResult=settings_form_email_logo": {
- "lang_key": "email_logo",
- "restriction": "email_manage",
- "keywords": [
- "email logo",
- "лого почты"
- ]
- },
- "app=core&module=settings&controller=email&searchResult=settings_form_email_out": {
- "lang_key": "email_out",
- "restriction": "email_manage",
- "keywords": [
- "email",
- "outgoing email",
- "site email",
- "почта",
- "исходящая почта"
- ]
- },
- "app=core&module=settings&controller=email&searchResult=settings_form_mail_method": {
- "lang_key": "mail_method",
- "restriction": "email_manage",
- "keywords": [
- "email",
- "mail",
- "smtp",
- "почта",
- "письмо"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_board_name": {
- "lang_key": "board_name",
- "restriction": "general_manage",
- "keywords": [
- "community name",
- "site name",
- "website name",
- "название сайта",
- "имя сайта"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_copyright_line": {
- "lang_key": "copyright_line",
- "restriction": "general_manage",
- "keywords": [
- "copyright",
- "license",
- "копирайт",
- "лицензия"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_show_home_link": {
- "lang_key": "show_home_link",
- "restriction": "general_manage",
- "keywords": [
- "breadcrumb",
- "navigation",
- "хлебные крошки",
- "навигация"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_site_address": {
- "lang_key": "site_address",
- "restriction": "general_manage",
- "keywords": [
- "address",
- "mail",
- "адрес",
- "почта"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_site_online": {
- "lang_key": "site_online",
- "restriction": "general_manage",
- "keywords": [
- "offline",
- "online",
- "в сети",
- "не в сети",
- "онлайн",
- "оффлайн"
- ]
- },
- "app=core&module=settings&controller=general&searchResult=form_upgrade_email": {
- "lang_key": "upgrade_email",
- "restriction": "general_manage",
- "keywords": [
- "update",
- "upgrade",
- "обновить",
- "обновление"
- ]
- },
- "app=core&module=settings&controller=licensekey": {
- "lang_key": "menu__core_settings_licensekey",
- "restriction": "licensekey_manage",
- "keywords": [
- "license key",
- "лицензионный ключ",
- "ключ лицензии"
- ]
- },
- "app=core&module=settings&controller=login": {
- "lang_key": "menu__core_settings_login",
- "restriction": "login_manage",
- "keywords": [
- "accounts",
- "facebook",
- "google",
- "https",
- "ips connect",
- "ldap",
- "locking",
- "login",
- "microsoft",
- "twitter",
- "windows",
- "аккаунты",
- "фейсбук",
- "гугл",
- "вконтакте",
- "логин",
- "авторизация",
- "твиттер",
- "окна"
- ]
- },
- "app=core&module=settings&controller=login&do=settings&searchResult=disable_anonymous": {
- "lang_key": "disable_anonymous",
- "restriction": "login_manage",
- "keywords": [
- "anonymous",
- "anonymous login",
- "аноним",
- "анонимный вход"
- ]
- },
- "app=core&module=settings&controller=login&do=settings&searchResult=form_logins_over_https": {
- "lang_key": "logins_over_https",
- "restriction": "login_manage",
- "keywords": [
- "https",
- "secure logins",
- "ssl",
- "защищённая авторизация"
- ]
- },
- "app=core&module=settings&controller=login&do=settings&searchResult=ipb_bruteforce_attempts": {
- "lang_key": "ipb_bruteforce_attempts",
- "restriction": "login_manage",
- "keywords": [
- "brute",
- "bruteforce",
- "failed",
- "logins",
- "логин",
- "авторизация",
- "брут"
- ]
- },
- "app=core&module=settings&controller=mfa": {
- "lang_key": "menu__core_settings_mfa",
- "restriction": "mfa_manage",
- "keywords": [
- "2 factor authentication",
- "2 factor verification",
- "2-factor authentication",
- "2-factor verification",
- "2fa",
- "account authentication",
- "account verification",
- "authentication",
- "google authentication",
- "google authenticator",
- "mfa",
- "multi factor authentication",
- "multi factor verification",
- "multi-factor authentication",
- "multi-factor verification",
- "multifactor authentication",
- "multifactor verification",
- "security",
- "security questions",
- "two factor authentication",
- "two factor verification",
- "two-factor authentication",
- "two-factor verification",
- "verification",
- "двухфакторная",
- "аутентификация",
- "верификация",
- "защита",
- "гугл",
- "мульти",
- "авторизация",
- "безопасность",
- "вопросы"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=attachment_image_size": {
- "lang_key": "attachment_image_size",
- "restriction": "posting_manage",
- "keywords": [
- "thumbnail dimensions",
- "thumbnail size",
- "размер миниатюры",
- "вес миниатюры"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=attachment_resample_size": {
- "lang_key": "attachment_resample_size",
- "restriction": "posting_manage",
- "keywords": [
- "image dimensions",
- "image size",
- "размер изображения",
- "вес изображения"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_allow_remote_images": {
- "lang_key": "allow_remote_images",
- "restriction": "posting_manage",
- "keywords": [
- "external images",
- "linked images",
- "remote images",
- "удвлённые изображения",
- "сторонние изображения",
- "внешние изображения"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_attach_allowed_types": {
- "lang_key": "attach_allowed_types",
- "restriction": null,
- "keywords": [
- "attachment restrictions",
- "attachments",
- "file types",
- "files",
- "файлы",
- "типы файлов",
- "вложения"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_edit_log": {
- "lang_key": "edit_log",
- "restriction": "posting_manage",
- "keywords": [
- "edit log",
- "editing",
- "изменение",
- "редактирование",
- "лог измнений"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_editor_embeds": {
- "lang_key": "editor_embeds",
- "restriction": "posting_manage",
- "keywords": [
- "disable",
- "embed",
- "embeds",
- "facebook",
- "youtube",
- "ютуб",
- "отключить",
- "фейсбук"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_flood_control": {
- "lang_key": "flood_control",
- "restriction": "posting_manage",
- "keywords": [
- "flood control",
- "контроль флуда",
- "флуд контроль"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_max_title_length": {
- "lang_key": "max_title_length",
- "restriction": "posting_manage",
- "keywords": [
- "title length",
- "title size",
- "длина заголовка",
- "длина названия",
- "размер заголовка"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_max_video_width": {
- "lang_key": "max_video_width",
- "restriction": "posting_manage",
- "keywords": [
- "video size",
- "video width",
- "videos",
- "видео",
- "размер видео",
- "вес видео"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_merge_concurrent_posts": {
- "lang_key": "merge_concurrent_posts",
- "restriction": null,
- "keywords": [
- "concurrent",
- "merge",
- "merging",
- "объединить",
- "объединение"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_reviews_rating_out_of": {
- "lang_key": "reviews_rating_out_of",
- "restriction": "posting_manage",
- "keywords": [
- "reviews",
- "stars",
- "звёзды",
- "отзывы",
- "звезда"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=form_topic_redirect_prune": {
- "lang_key": "topic_redirect_prune",
- "restriction": "posting_manage",
- "keywords": [
- "moved links",
- "shadow topics",
- "перемещённые темы",
- "ссылка перемещения"
- ]
- },
- "app=core&module=settings&controller=posting&searchResult=remote_image_proxy": {
- "lang_key": "remote_image_proxy",
- "restriction": "posting_manage",
- "keywords": [
- "external image proxy",
- "https",
- "image proxy",
- "proxy",
- "remote image proxy",
- "ssl",
- "прокси"
- ]
- },
- "app=core&module=settings&controller=posting&tab=acronymExpansion": {
- "lang_key": "acronym_expansion",
- "restriction": "posting_manage_acronym",
- "keywords": [
- "abbreviations",
- "acronyms",
- "акроним",
- "сокращение",
- "аббревиатура"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=attachment_image_size": {
- "lang_key": "attachment_image_size",
- "restriction": null,
- "keywords": [
- "attachments",
- "images",
- "thumbnails",
- "uploads",
- "вложения",
- "прикрепления",
- "загрузки",
- "изображения"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_allow_remote_images": {
- "lang_key": "allow_remote_images",
- "restriction": null,
- "keywords": [
- "external",
- "images",
- "linked",
- "remote",
- "внешний",
- "удалённый",
- "изображение"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_attach_allowed_types": {
- "lang_key": "attach_allowed_types",
- "restriction": null,
- "keywords": [
- "attachments",
- "extensions",
- "uploads",
- "вложения",
- "прикрепления",
- "загрузки",
- "расширения"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_edit_log": {
- "lang_key": "edit_log",
- "restriction": null,
- "keywords": [
- "edit",
- "log",
- "изменить",
- "журнал",
- "лог"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_flood_control": {
- "lang_key": "flood_control",
- "restriction": null,
- "keywords": [
- "flood",
- "limit",
- "posting",
- "флуд",
- "лимит",
- "постинг",
- "публикация"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_max_title_length": {
- "lang_key": "max_title_length",
- "restriction": null,
- "keywords": [
- "titles",
- "заголовки",
- "названия"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_max_video_width": {
- "lang_key": "max_video_width",
- "restriction": null,
- "keywords": [
- "embedded",
- "media",
- "videos",
- "vimeo",
- "youtube",
- "ютуб",
- "медиа",
- "видео",
- "встроить"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_merge_concurrent_posts": {
- "lang_key": "merge_concurrent_posts",
- "restriction": null,
- "keywords": [
- "combine",
- "concurrent",
- "merge",
- "смешать",
- "объединить"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_reviews_rating_out_of": {
- "lang_key": "reviews_rating_out_of",
- "restriction": null,
- "keywords": [
- "ratings",
- "reviews",
- "stars",
- "звёзды",
- "звезда",
- "рейтинг",
- "рейтинги",
- "отзывы"
- ]
- },
- "app=core&module=settings&controller=posting&tab=general&searchResult=form_topic_redirect_prune": {
- "lang_key": "topic_redirect_prune",
- "restriction": null,
- "keywords": [
- "moved",
- "shadow",
- "перемещено",
- "переместить",
- "тень"
- ]
- },
- "app=core&module=settings&controller=posting&tab=polls": {
- "lang_key": "polls",
- "restriction": "posting_manage_polls",
- "keywords": [
- "polls",
- "question",
- "vote",
- "голос",
- "голосование",
- "опрос",
- "вопрос"
- ]
- },
- "app=core&module=settings&controller=posting&tab=profanityFilters": {
- "lang_key": "profanity",
- "restriction": "posting_manage_profanity",
- "keywords": [
- "bad words",
- "profanity",
- "swear words",
- "плохие слова",
- "мат",
- "нецензурные слова",
- "ненормативная лексика"
- ]
- },
- "app=core&module=settings&controller=posting&tab=tags": {
- "lang_key": "tags",
- "restriction": "posting_manage_tags",
- "keywords": [
- "prefixes",
- "tagging",
- "tags",
- "теги",
- "префиксы",
- "тегирование"
- ]
- },
- "app=core&module=settings&controller=posting&tab=urlFilters": {
- "lang_key": "ipb_url_filter_option",
- "restriction": "posting_manage_url",
- "keywords": [
- "blacklist",
- "links",
- "urls",
- "whitelist",
- "белый список",
- "чёрный список",
- "ссылки"
- ]
- },
- "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=form_links_external": {
- "lang_key": "links_external",
- "restriction": "posting_manage",
- "keywords": [
- "blank",
- "link",
- "tab",
- "target",
- "window",
- "окно",
- "таргет",
- "ссылка",
- "вкладка",
- "открыть в новом окне"
- ]
- },
- "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=ipb_url_blacklist": {
- "lang_key": "ipb_url_blacklist",
- "restriction": "posting_manage",
- "keywords": [
- "blacklist",
- "чёрный список"
- ]
- },
- "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=posts_add_nofollow": {
- "lang_key": "posts_add_nofollow",
- "restriction": "posting_manage",
- "keywords": [
- "nofollow",
- "pagerank",
- "отписаться",
- "отписка"
- ]
- },
- "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=posts_add_nofollow_exclude": {
- "lang_key": "posts_add_nofollow_exclude",
- "restriction": null,
- "keywords": [
- "follow",
- "nofollow",
- "pagerank",
- "следовать",
- "подписаться",
- "отписаться",
- "отписка"
- ]
- },
- "app=core&module=settings&controller=securityquestions": {
- "lang_key": "menu__core_settings_securityquestions",
- "restriction": "securityquestions_manage",
- "keywords": [
- "answers",
- "questions",
- "security",
- "вопросы",
- "ответы",
- "безопасность",
- "защита"
- ]
- },
- "app=core&module=settings&controller=terms": {
- "lang_key": "menu__core_settings_terms",
- "restriction": "terms_manage",
- "keywords": [
- "conditions",
- "guidelines",
- "legal",
- "privacy policy",
- "rules",
- "terms",
- "условия",
- "правила",
- "приватность",
- "политика конденфициальности"
- ]
- },
- "app=core&module=settings&controller=terms&searchResult=form_guest_terms_bar": {
- "lang_key": "guest_terms_bar",
- "restriction": "terms_manage",
- "keywords": [
- "terms bar",
- "полоса"
- ]
- },
- "app=core&module=staff&controller=admin": {
- "lang_key": "menu__core_staff_admin",
- "restriction": "restrictions_manage",
- "keywords": [
- "acp",
- "administrators",
- "admins",
- "restrictions",
- "staff",
- "акп",
- "админцентр",
- "ограничения",
- "модераторы",
- "администраторы"
- ]
- },
- "app=core&module=staff&controller=admin&do=actionLogs": {
- "lang_key": "acplogs",
- "restriction": "restrictions_adminlogs",
- "keywords": [
- "acp logs",
- "admin logs",
- "logs",
- "логи",
- "журналы",
- "жерналы админа"
- ]
- },
- "app=core&module=staff&controller=admin&do=loginLogs": {
- "lang_key": "menu__core_staff_admin",
- "restriction": "restrictions_manage",
- "keywords": [
- "acp logins",
- "admin logins",
- "входы в админку",
- "входы в админцентр"
- ]
- },
- "app=core&module=staff&controller=directory": {
- "lang_key": "menu__core_staff_directory",
- "restriction": "leaders_manage",
- "keywords": [
- "leaders",
- "moderators",
- "staff",
- "модераторы",
- "сотрудники",
- "лидеры",
- "команда"
- ]
- },
- "app=core&module=staff&controller=moderators": {
- "lang_key": "menu__core_staff_moderators",
- "restriction": "moderators_manage",
- "keywords": [
- "moderators",
- "staff",
- "модераторы",
- "сотрудники"
- ]
- },
- "app=core&module=staff&controller=moderators&do=actionLogs": {
- "lang_key": "modlogs",
- "restriction": "restrictions_moderatorlogs",
- "keywords": [
- "logs",
- "mod logs",
- "moderator logs",
- "staff logs",
- "журналы",
- "логи",
- "логи модератора",
- "жерналы модератора"
- ]
- },
- "app=core&module=stats&controller=pmstats": {
- "lang_key": "menu__core_members_pmstats",
- "restriction": null,
- "keywords": [
- "message statistics",
- "Messenger Statistics",
- "private messages",
- "статистика сообщений",
- "личные сообщения"
- ]
- },
- "app=core&module=stats&controller=registrationstats": {
- "lang_key": "menu__core_members_registrationstats",
- "restriction": null,
- "keywords": [
- "registration statistics",
- "Registrations",
- "регистрации",
- "регистрация",
- "статистика регистраций"
- ]
- },
- "app=core&module=support&controller=errorLogs": {
- "lang_key": "error_logs",
- "restriction": null,
- "keywords": [
- "error log",
- "errors",
- "logs",
- "логи",
- "журналы",
- "журнал ошибок",
- "ошибки"
- ]
- },
- "app=core&module=support&controller=sql": {
- "lang_key": "sql_toolbox",
- "restriction": "sql_toolbox",
- "keywords": [
- "sql toolbox",
- "инструменты"
- ]
- },
- "app=core&module=support&controller=support": {
- "lang_key": "menu__core_support_dashboard",
- "restriction": "support_dashboard",
- "keywords": [
- "diagnose",
- "diagnostics",
- "documentation",
- "help",
- "phpinfo",
- "server",
- "support",
- "system",
- "tech support",
- "technical support",
- "troubleshoot",
- "неполадки",
- "устранение неполадок",
- "система",
- "поддержка",
- "сервер",
- "помощь",
- "документация",
- "диагностика"
- ]
- },
- "app=core&module=support&controller=systemLogs": {
- "lang_key": "system_logs",
- "restriction": null,
- "keywords": [
- "driver errors",
- "logs",
- "sql error logs",
- "sql logs",
- "system logs",
- "ошибка драйвера",
- "журналы",
- "логи",
- "ошибки sql",
- "системные журналы"
- ]
- }
+{
+ "app=core&module=applications&controller=api": {
+ "lang_key": "menu__core_applications_api",
+ "restriction": "api_manage",
+ "keywords": [
+ "api",
+ "rest api"
+ ]
+ },
+ "app=core&module=applications&controller=api&tab=apiKeys": {
+ "lang_key": "api_keys",
+ "restriction": "api_manage",
+ "keywords": [
+ "api keys"
+ ]
+ },
+ "app=core&module=applications&controller=api&tab=apiLogs": {
+ "lang_key": "api_logs",
+ "restriction": "api_logs",
+ "keywords": [
+ "api logs"
+ ]
+ },
+ "app=core&module=applications&controller=api&tab=apiReference": {
+ "lang_key": "api_reference",
+ "restriction": "api_reference",
+ "keywords": [
+ "api documentation",
+ "api reference"
+ ]
+ },
+ "app=core&module=applications&controller=applications": {
+ "lang_key": "menu__core_applications_applications",
+ "restriction": "app_manage",
+ "keywords": [
+ "applications",
+ "install",
+ "modules",
+ "offline",
+ "online"
+ ]
+ },
+ "app=core&module=applications&controller=enhancements": {
+ "lang_key": "menu__core_applications_enhancements",
+ "restriction": "enhancements_manage",
+ "keywords": [
+ "bulk mail",
+ "email",
+ "enhancements",
+ "geo",
+ "maps",
+ "maxmind",
+ "sendgrid",
+ "spam",
+ "viglink"
+ ]
+ },
+ "app=core&module=applications&controller=enhancements&do=edit&id=core_GoogleMaps&searchResult=form_googlemaps": {
+ "lang_key": "googlemaps",
+ "restriction": "enhancements_manage",
+ "keywords": [
+ "google",
+ "maps"
+ ]
+ },
+ "app=core&module=applications&controller=enhancements&do=edit&id=core_Sparkpost": {
+ "lang_key": "enhancements__core_Sparkpost",
+ "restriction": "enhancements_manage",
+ "keywords": [
+ "bulk mail",
+ "email",
+ "sparkpost"
+ ]
+ },
+ "app=core&module=applications&controller=enhancements&do=edit&id=core_Viglink&searchResult=form_viglink_account_type": {
+ "lang_key": "viglink_account_type",
+ "restriction": "enhancements_manage",
+ "keywords": [
+ "advertising",
+ "viglink"
+ ]
+ },
+ "app=core&module=applications&controller=plugins": {
+ "lang_key": "menu__core_applications_plugins",
+ "restriction": "plugins_view",
+ "keywords": [
+ "add-ons",
+ "addons",
+ "hooks",
+ "install",
+ "mod",
+ "modification",
+ "plugins"
+ ]
+ },
+ "app=core&module=bulkmail&controller=bulkmail": {
+ "lang_key": "menu__core_bulkmail_bulkmail",
+ "restriction": "bulkmail_manage",
+ "keywords": [
+ "announce",
+ "blast",
+ "bulk",
+ "bulk mail",
+ "campaign",
+ "email",
+ "mail",
+ "mass",
+ "newsletter",
+ "sendgrid"
+ ]
+ },
+ "app=core&module=customization&controller=emails": {
+ "lang_key": "menu__core_customization_emails",
+ "restriction": "emails_manage",
+ "keywords": [
+ "email content"
+ ]
+ },
+ "app=core&module=customization&controller=themes": {
+ "lang_key": "menu__core_customization_themes",
+ "restriction": "theme_sets_manage",
+ "keywords": [
+ "design",
+ "look",
+ "skins",
+ "themes"
+ ]
+ },
+ "app=core&module=customization&controller=themes&do=designersmode&searchResult=form_theme_designers_mode": {
+ "lang_key": "theme_designers_mode",
+ "restriction": "theme_templates_manage",
+ "keywords": [
+ "designers mode"
+ ]
+ },
+ "app=core&module=discovery&controller=rss": {
+ "lang_key": "menu__core_discovery_rss",
+ "restriction": null,
+ "keywords": [
+ "feed",
+ "rss"
+ ]
+ },
+ "app=core&module=discovery&controller=search": {
+ "lang_key": "menu__core_discovery_search",
+ "restriction": "search_manage",
+ "keywords": [
+ "rebuild search",
+ "rebuild search index",
+ "reindex",
+ "reindex search",
+ "reindex search results",
+ "search",
+ "search index",
+ "search index rebuild",
+ "searching"
+ ]
+ },
+ "app=core&module=discovery&controller=search&searchResult=search_index_timeframe": {
+ "lang_key": "search_index_timeframe",
+ "restriction": "search_manage",
+ "keywords": [
+ "search",
+ "search content",
+ "search cutoff",
+ "search limit",
+ "search results"
+ ]
+ },
+ "app=core&module=editor&controller=emoticons": {
+ "lang_key": "menu__core_editor_emoticons",
+ "restriction": "emoticons_manage",
+ "keywords": [
+ "emoticons",
+ "smileys",
+ "smilies"
+ ]
+ },
+ "app=core&module=editor&controller=settings": {
+ "lang_key": "menu__core_editor_settings",
+ "restriction": "settings_manage",
+ "keywords": [
+ "editor"
+ ]
+ },
+ "app=core&module=editor&controller=settings&searchResult=form_editor_paragraph_padding": {
+ "lang_key": "editor_paragraph_padding",
+ "restriction": "settings_manage",
+ "keywords": [
+ "enter",
+ "paragraph",
+ "return"
+ ]
+ },
+ "app=core&module=editor&controller=settings&searchResult=form_editor_paste_behaviour": {
+ "lang_key": "editor_paste_behaviour",
+ "restriction": "settings_manage",
+ "keywords": [
+ "formatting",
+ "paste",
+ "posting",
+ "remove"
+ ]
+ },
+ "app=core&module=editor&controller=toolbar": {
+ "lang_key": "editor_customization",
+ "restriction": "toolbar_manage",
+ "keywords": [
+ "bbcode",
+ "buttons",
+ "comment",
+ "editor",
+ "mobile",
+ "phone",
+ "post",
+ "responsive",
+ "tablet",
+ "toolbars",
+ "wysiwyg"
+ ]
+ },
+ "app=core&module=languages&controller=languages": {
+ "lang_key": "menu__core_languages_languages",
+ "restriction": null,
+ "keywords": [
+ "languages",
+ "locales",
+ "translate",
+ "translations"
+ ]
+ },
+ "app=core&module=members&controller=groups": {
+ "lang_key": "menu__core_members_groups",
+ "restriction": "groups_manage",
+ "keywords": [
+ "groups"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=3_gbw_post_highlight": {
+ "lang_key": "gbw_post_highlight",
+ "restriction": "groups_manage",
+ "keywords": [
+ "highlight"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_attach": {
+ "lang_key": "g_attach",
+ "restriction": "groups_manage",
+ "keywords": [
+ "attachments",
+ "uploads"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_avoid_flood": {
+ "lang_key": "g_avoid_flood",
+ "restriction": "groups_manage",
+ "keywords": [
+ "flood control",
+ "post flood control",
+ "posting flood control"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_avoid_q": {
+ "lang_key": "g_avoid_q",
+ "restriction": "groups_manage",
+ "keywords": [
+ "hidden",
+ "hide",
+ "hiding",
+ "mod queue",
+ "moderation",
+ "queue"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_bypass_badwords": {
+ "lang_key": "g_bypass_badwords",
+ "restriction": "groups_manage",
+ "keywords": [
+ "bad words",
+ "profanity",
+ "swear words"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_delete_own_posts": {
+ "lang_key": "g_delete_own_posts",
+ "restriction": "groups_manage",
+ "keywords": [
+ "delete",
+ "deleting"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_edit_posts": {
+ "lang_key": "g_edit_posts",
+ "restriction": "groups_manage",
+ "keywords": [
+ "edit",
+ "editing"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_edit_profile": {
+ "lang_key": "g_edit_profile",
+ "restriction": "groups_manage",
+ "keywords": [
+ "edit profile",
+ "profiles"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_icon": {
+ "lang_key": "g_icon",
+ "restriction": "groups_manage",
+ "keywords": [
+ "group icon"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_post_polls": {
+ "lang_key": "g_post_polls",
+ "restriction": "groups_manage",
+ "keywords": [
+ "polls"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_ppd_limit": {
+ "lang_key": "g_ppd_limit",
+ "restriction": "groups_manage",
+ "keywords": [
+ "content restriction",
+ "max content",
+ "max posts",
+ "per day",
+ "positing limit",
+ "positing restriction"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_prefixsuffix": {
+ "lang_key": "g_prefixsuffix",
+ "restriction": "groups_manage",
+ "keywords": [
+ "group color",
+ "group colour"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_rep_max_positive": {
+ "lang_key": "g_rep_max_positive",
+ "restriction": "groups_manage",
+ "keywords": [
+ "karma",
+ "like",
+ "reputation"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_topic_rate_setting": {
+ "lang_key": "g_topic_rate_setting",
+ "restriction": "groups_manage",
+ "keywords": [
+ "rate",
+ "ratings"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_view_board": {
+ "lang_key": "g_view_board",
+ "restriction": "groups_manage",
+ "keywords": [
+ "access",
+ "allowed to enter community",
+ "allowed to enter site",
+ "allowed to view community",
+ "allowed to view site",
+ "user access site"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_g_vote_polls": {
+ "lang_key": "g_vote_polls",
+ "restriction": "groups_manage",
+ "keywords": [
+ "polls"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_disable_prefixes": {
+ "lang_key": "gbw_disable_prefixes",
+ "restriction": "groups_manage",
+ "keywords": [
+ "prefixes",
+ "tagging",
+ "tags"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_disable_tagging": {
+ "lang_key": "gbw_disable_tagging",
+ "restriction": "groups_manage",
+ "keywords": [
+ "tagging",
+ "tags"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_no_report": {
+ "lang_key": "gbw_no_report",
+ "restriction": "groups_manage",
+ "keywords": [
+ "report",
+ "reporting"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_no_status_import": {
+ "lang_key": "gbw_no_status_import",
+ "restriction": "groups_manage",
+ "keywords": [
+ "facebook status",
+ "profile status",
+ "status updates",
+ "statuses",
+ "twitter status"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_no_status_update": {
+ "lang_key": "gbw_no_status_update",
+ "restriction": "groups_manage",
+ "keywords": [
+ "profile status",
+ "status updates",
+ "statuses"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_post_highlight": {
+ "lang_key": "gbw_post_highlight",
+ "restriction": "groups_manage",
+ "keywords": [
+ "highlight",
+ "highlight post",
+ "replies"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_soft_delete_own": {
+ "lang_key": "gbw_soft_delete_own",
+ "restriction": "groups_manage",
+ "keywords": [
+ "hide",
+ "hiding"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=4_gbw_view_reps": {
+ "lang_key": "gbw_view_reps",
+ "restriction": "groups_manage",
+ "keywords": [
+ "karma",
+ "like",
+ "reputation"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_access_offline": {
+ "lang_key": "g_access_offline",
+ "restriction": "groups_manage",
+ "keywords": [
+ "offline access",
+ "site offline"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_append_edit": {
+ "lang_key": "g_append_edit",
+ "restriction": "groups_manage",
+ "keywords": [
+ "edit",
+ "silent edit"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_attach_max": {
+ "lang_key": "g_attach_max",
+ "restriction": "groups_manage",
+ "keywords": [
+ "attachments",
+ "storage",
+ "uploads"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_attach_per_post": {
+ "lang_key": "g_attach_per_post",
+ "restriction": "groups_manage",
+ "keywords": [
+ "upload per article",
+ "upload per blog",
+ "upload per content item",
+ "upload per post",
+ "upload per topic"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_can_msg_attach": {
+ "lang_key": "g_can_msg_attach",
+ "restriction": "groups_manage",
+ "keywords": [
+ "messenger attachments",
+ "messenger images",
+ "messenger upload",
+ "pm attachments",
+ "pm images",
+ "pm upload"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_dohtml": {
+ "lang_key": "g_dohtml",
+ "restriction": "groups_manage",
+ "keywords": [
+ "html posting",
+ "post html"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_edit_cutoff": {
+ "lang_key": "g_edit_cutoff",
+ "restriction": "groups_manage",
+ "keywords": [
+ "edit restriction",
+ "edit time",
+ "edit timing"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_hide_online_list": {
+ "lang_key": "g_hide_online_list",
+ "restriction": "groups_manage",
+ "keywords": [
+ "online list",
+ "show user list",
+ "show user online"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_max_bgimg_upload": {
+ "lang_key": "g_max_bgimg_upload",
+ "restriction": "groups_manage",
+ "keywords": [
+ "cover photo size",
+ "profile cover photo size"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_max_messages": {
+ "lang_key": "g_max_messages",
+ "restriction": "groups_manage",
+ "keywords": [
+ "messenger storage",
+ "personal messenger storage",
+ "pm storage",
+ "pms"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_photo_max_vars_size": {
+ "lang_key": "g_photo_max_vars_size",
+ "restriction": "groups_manage",
+ "keywords": [
+ "photo size",
+ "photos",
+ "profile photo size"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_photo_max_vars_wh": {
+ "lang_key": "g_photo_max_vars_wh",
+ "restriction": "groups_manage",
+ "keywords": [
+ "photo size",
+ "photos",
+ "profile photo size"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_pm_perday": {
+ "lang_key": "g_pm_perday",
+ "restriction": "groups_manage",
+ "keywords": [
+ "new pms",
+ "personal messages",
+ "personal messenger",
+ "pm limit",
+ "pms"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_ppd_unit": {
+ "lang_key": "g_ppd_unit",
+ "restriction": "groups_manage",
+ "keywords": [
+ "positing limit"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_search_flood": {
+ "lang_key": "g_search_flood",
+ "restriction": "groups_manage",
+ "keywords": [
+ "search flood control"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_sig_max_image_size": {
+ "lang_key": "g_sig_max_image_size",
+ "restriction": "groups_manage",
+ "keywords": [
+ "signature images",
+ "signatures"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_sig_max_images": {
+ "lang_key": "g_sig_max_images",
+ "restriction": "groups_manage",
+ "keywords": [
+ "signature images",
+ "signatures"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_sig_max_lines": {
+ "lang_key": "g_sig_max_lines",
+ "restriction": "groups_manage",
+ "keywords": [
+ "signature length",
+ "signature size",
+ "signature text",
+ "signatures"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_sig_max_urls": {
+ "lang_key": "g_sig_max_urls",
+ "restriction": "groups_manage",
+ "keywords": [
+ "signature links",
+ "signatures"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=g_use_signatures": {
+ "lang_key": "g_use_signatures",
+ "restriction": "groups_manage",
+ "keywords": [
+ "signatures"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=gbw_allow_upload_bgimage": {
+ "lang_key": "gbw_allow_upload_bgimage",
+ "restriction": "groups_manage",
+ "keywords": [
+ "cover photo",
+ "profile cover photo"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=gbw_cannot_be_ignored": {
+ "lang_key": "can_be_ignored",
+ "restriction": "groups_manage",
+ "keywords": [
+ "ignore",
+ "ignored users"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=gbw_delete_attachments": {
+ "lang_key": "gbw_delete_attachments",
+ "restriction": "groups_manage",
+ "keywords": [
+ "attachments",
+ "delete attachments",
+ "uploads"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=group_promotion": {
+ "lang_key": "group_promotion",
+ "restriction": "groups_manage",
+ "keywords": [
+ "group promote",
+ "group promotion",
+ "group upgrade",
+ "member promote",
+ "member promotion",
+ "member upgrade",
+ "promote",
+ "promotion"
+ ]
+ },
+ "app=core&module=members&controller=groups&searchResult=group_username": {
+ "lang_key": "group_username",
+ "restriction": "groups_manage",
+ "keywords": [
+ "display name changes",
+ "name changes",
+ "user name changes",
+ "username changes"
+ ]
+ },
+ "app=core&module=members&controller=ip": {
+ "lang_key": "menu__core_members_ip",
+ "restriction": "membertools_ip",
+ "keywords": [
+ "address",
+ "IP",
+ "lookup"
+ ]
+ },
+ "app=core&module=members&controller=members": {
+ "lang_key": "menu__core_members_members",
+ "restriction": null,
+ "keywords": [
+ "accounts",
+ "administrators",
+ "banned",
+ "locked",
+ "members",
+ "spammers",
+ "validating"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_allow_admin_mails": {
+ "lang_key": "allow_admin_mails",
+ "restriction": null,
+ "keywords": [
+ "Email",
+ "Send Email",
+ "Send News"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_auto_track": {
+ "lang_key": "auto_track",
+ "restriction": null,
+ "keywords": [
+ "Auto",
+ "Automatically",
+ "Content",
+ "Follow"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_group": {
+ "lang_key": "group",
+ "restriction": null,
+ "keywords": [
+ "Group"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_ips_address_mail": {
+ "lang_key": "ips_address_mail",
+ "restriction": null,
+ "keywords": [
+ "Email Address"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_ips_member_name": {
+ "lang_key": "ips_member_name",
+ "restriction": null,
+ "keywords": [
+ "Display Name"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_member_content_items": {
+ "lang_key": "member_content_items",
+ "restriction": null,
+ "keywords": [
+ "Content Item",
+ "Recount"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_member_reputation": {
+ "lang_key": "member_reputation",
+ "restriction": null,
+ "keywords": [
+ "Recount",
+ "Reputation Level"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_member_title": {
+ "lang_key": "member_title",
+ "restriction": null,
+ "keywords": [
+ "Member Title"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_member_warnings": {
+ "lang_key": "member_warnings",
+ "restriction": null,
+ "keywords": [
+ "Warning Points"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_password": {
+ "lang_key": "password",
+ "restriction": null,
+ "keywords": [
+ "Change Password",
+ "Password"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_secondary_groups": {
+ "lang_key": "secondary_groups",
+ "restriction": null,
+ "keywords": [
+ "Secondary Groups"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_skin": {
+ "lang_key": "skin",
+ "restriction": null,
+ "keywords": [
+ "Theme"
+ ]
+ },
+ "app=core&module=members&controller=members&searchResult=form_view_sigs": {
+ "lang_key": "view_sigs",
+ "restriction": null,
+ "keywords": [
+ "Signatures",
+ "View"
+ ]
+ },
+ "app=core&module=membersettings&controller=ban": {
+ "lang_key": "menu__core_membersettings_ban",
+ "restriction": "ban_manage",
+ "keywords": [
+ "banned",
+ "bans",
+ "email",
+ "ip address",
+ "register",
+ "username"
+ ]
+ },
+ "app=core&module=membersettings&controller=notifications": {
+ "lang_key": "menu__core_membersettings_notifications",
+ "restriction": "notifications_manage",
+ "keywords": [
+ "notifications"
+ ]
+ },
+ "app=core&module=membersettings&controller=notifications&do=settings&searchResult=auto_follow_new_content": {
+ "lang_key": "auto_follow_new_content",
+ "restriction": "notifications_manage",
+ "keywords": [
+ "auto",
+ "automatically",
+ "content",
+ "follow"
+ ]
+ },
+ "app=core&module=membersettings&controller=notifications&do=settings&searchResult=auto_follow_replied_to": {
+ "lang_key": "auto_follow_replied_to",
+ "restriction": "notifications_manage",
+ "keywords": [
+ "auto",
+ "automatically",
+ "content",
+ "follow"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles": {
+ "lang_key": "menu__core_membersettings_profiles",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "about me",
+ "custom profile fields"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=fields": {
+ "lang_key": "profile_fields",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "custom fields",
+ "custom profile",
+ "custom profile fields",
+ "edit fields",
+ "edit profile fields",
+ "profile fields"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_allow_gravatars": {
+ "lang_key": "allow_gravatars",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "gravatar"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_profile_comments": {
+ "lang_key": "profile_comments",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "profile comments",
+ "profile statuses"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_signatures_enabled": {
+ "lang_key": "signatures_enabled",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "signatures"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=form_username_characters": {
+ "lang_key": "username_characters",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "username characters"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=profile_comment_approval": {
+ "lang_key": "profile_comment_approval",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "profile comments",
+ "profile statuses"
+ ]
+ },
+ "app=core&module=membersettings&controller=profiles&tab=settings&searchResult=user_name_length": {
+ "lang_key": "user_name_length",
+ "restriction": "profiles_manage",
+ "keywords": [
+ "display name length",
+ "display name size",
+ "maximum display name length",
+ "maximum display name size",
+ "maximum username length",
+ "maximum username size",
+ "minimum display name length",
+ "minimum display name size",
+ "minimum username length",
+ "minimum username size",
+ "username length",
+ "username size"
+ ]
+ },
+ "app=core&module=membersettings&controller=ranks": {
+ "lang_key": "menu__core_membersettings_ranks",
+ "restriction": "ranks_manage",
+ "keywords": [
+ "badges",
+ "ranks",
+ "titles"
+ ]
+ },
+ "app=core&module=membersettings&controller=registration": {
+ "lang_key": "menu__core_membersettings_registration",
+ "restriction": "registration_manage",
+ "keywords": [
+ "approve",
+ "confirmation",
+ "coppa",
+ "registration",
+ "validate",
+ "validation"
+ ]
+ },
+ "app=core&module=membersettings&controller=registration&searchResult=form_allow_reg": {
+ "lang_key": "allow_reg",
+ "restriction": "registration_manage",
+ "keywords": [
+ "allow registrations",
+ "new registrations",
+ "registration",
+ "registrations"
+ ]
+ },
+ "app=core&module=membersettings&controller=registration&searchResult=new_reg_notify": {
+ "lang_key": "new_reg_notify",
+ "restriction": "registration_manage",
+ "keywords": [
+ "email on",
+ "email on new registration",
+ "new registrations",
+ "notify",
+ "notify new registration",
+ "registrations"
+ ]
+ },
+ "app=core&module=membersettings&controller=registration&searchResult=use_coppa": {
+ "lang_key": "use_coppa",
+ "restriction": "registration_manage",
+ "keywords": [
+ "coppa"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation": {
+ "lang_key": "menu__core_membersettings_reputation",
+ "restriction": "reps_manage",
+ "keywords": [
+ "karma",
+ "likes",
+ "reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=form_reputation_enabled": {
+ "lang_key": "reputation_enabled",
+ "restriction": "reps_manage",
+ "keywords": [
+ "Likes",
+ "negative reputation",
+ "positive reputation",
+ "Reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_can_self_vote": {
+ "lang_key": "reputation_can_self_vote",
+ "restriction": "reps_settings",
+ "keywords": [
+ "like own",
+ "like self",
+ "own",
+ "rep self",
+ "reputation self",
+ "self",
+ "self reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_highlight": {
+ "lang_key": "reputation_highlight",
+ "restriction": "reps_manage",
+ "keywords": [
+ "popular"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_leaderboard_default_tab": {
+ "lang_key": "reputation_leaderboard_default_tab",
+ "restriction": "reps_manage",
+ "keywords": [
+ "leaderboard",
+ "member list"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_leaderboard_on": {
+ "lang_key": "reputation_leaderboard_on",
+ "restriction": "reps_manage",
+ "keywords": [
+ "leaderboard",
+ "reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_max_members": {
+ "lang_key": "reputation_max_members",
+ "restriction": "reps_manage",
+ "keywords": [
+ "top members"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_protected_groups": {
+ "lang_key": "reputation_protected_groups",
+ "restriction": "reps_settings",
+ "keywords": [
+ "no likes",
+ "no reputation",
+ "protected"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_content": {
+ "lang_key": "reputation_show_content",
+ "restriction": "reps_settings",
+ "keywords": [
+ "show likes",
+ "show reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_days_won_trophy": {
+ "lang_key": "reputation_show_days_won_trophy",
+ "restriction": "reps_manage",
+ "keywords": [
+ "days won"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_show_profile": {
+ "lang_key": "reputation_show_profile",
+ "restriction": "reps_manage",
+ "keywords": [
+ "show likes",
+ "show reputation"
+ ]
+ },
+ "app=core&module=membersettings&controller=reputation&searchResult=reputation_timezone": {
+ "lang_key": "reputation_timezone",
+ "restriction": "reps_manage",
+ "keywords": [
+ "leaderboard",
+ "timezone"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam": {
+ "lang_key": "menu__core_membersettings_spam",
+ "restriction": "spam_manage",
+ "keywords": [
+ "spam",
+ "spammers"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&do=serviceLogs": {
+ "lang_key": "spamlogs",
+ "restriction": "spam_service_log",
+ "keywords": [
+ "spam logs"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&searchResult=bot_antispam_type": {
+ "lang_key": "bot_antispam_type",
+ "restriction": "spam_manage",
+ "keywords": [
+ "bot prevention",
+ "CAPTCHA",
+ "security question"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&searchResult=guest_captcha": {
+ "lang_key": "guest_captcha_search_target",
+ "restriction": "spam_manage",
+ "keywords": [
+ "delete guest content",
+ "delete guest posts",
+ "guest content",
+ "guest posts"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&tab=captcha": {
+ "lang_key": "spamprevention_captcha",
+ "restriction": "spam_manage",
+ "keywords": [
+ "captcha",
+ "keycaptcha",
+ "recaptcha"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&tab=flagging": {
+ "lang_key": "spamprevention_flagging",
+ "restriction": "spam_manage",
+ "keywords": [
+ "flagging"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&tab=qanda": {
+ "lang_key": "qanda_settings",
+ "restriction": "qanda_manage",
+ "keywords": [
+ "answers",
+ "challenge",
+ "question",
+ "registration"
+ ]
+ },
+ "app=core&module=membersettings&controller=spam&tab=service": {
+ "lang_key": "enhancements__core_SpamMonitoring",
+ "restriction": "spam_manage",
+ "keywords": [
+ "ips spam monitor",
+ "ips spam monitoring",
+ "ips spam service"
+ ]
+ },
+ "app=core&module=membersettings&controller=warnings": {
+ "lang_key": "menu__core_membersettings_warnings",
+ "restriction": "warn_settings",
+ "keywords": [
+ "acknowledge",
+ "actions",
+ "reasons",
+ "warnings"
+ ]
+ },
+ "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=form_warn_on": {
+ "lang_key": "warn_on",
+ "restriction": "warn_settings",
+ "keywords": [
+ "warning",
+ "warning setting",
+ "warning system"
+ ]
+ },
+ "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warn_protected": {
+ "lang_key": "warn_protected",
+ "restriction": "warn_settings",
+ "keywords": [
+ "no warnings",
+ "protected"
+ ]
+ },
+ "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warn_show_own": {
+ "lang_key": "warn_show_own",
+ "restriction": "warn_settings",
+ "keywords": [
+ "own"
+ ]
+ },
+ "app=core&module=membersettings&controller=warnings&tab=settings&searchResult=warnings_acknowledge": {
+ "lang_key": "warnings_acknowledge",
+ "restriction": "warn_settings",
+ "keywords": [
+ "acknowledge"
+ ]
+ },
+ "app=core&module=overview&controller=files": {
+ "lang_key": "menu__core_overview_files",
+ "restriction": "files_view",
+ "keywords": [
+ "attachments",
+ "files",
+ "storage",
+ "uploads"
+ ]
+ },
+ "app=core&module=overview&controller=files&do=imagesettings": {
+ "lang_key": "image_settings",
+ "restriction": "files_view",
+ "keywords": [
+ "gd",
+ "imagemagick"
+ ]
+ },
+ "app=core&module=overview&controller=files&do=imagesettings&searchResult=form_image_jpg_quality": {
+ "lang_key": "image_jpg_quality",
+ "restriction": "files_view",
+ "keywords": [
+ "jpeg",
+ "jpg"
+ ]
+ },
+ "app=core&module=overview&controller=files&do=imagesettings&searchResult=image_png_quality_gd": {
+ "lang_key": "image_png_quality_gd",
+ "restriction": "files_view",
+ "keywords": [
+ "png"
+ ]
+ },
+ "app=core&module=overview&controller=files&do=settings": {
+ "lang_key": "menu__core_overview_files",
+ "restriction": "files_view",
+ "keywords": [
+ "amazon",
+ "files",
+ "ftp",
+ "photos",
+ "storage",
+ "uploads"
+ ]
+ },
+ "app=core&module=overview&controller=security": {
+ "lang_key": "menu__core_overview_security",
+ "restriction": "security_manage",
+ "keywords": [
+ "acp",
+ "admin",
+ "conf",
+ "hack",
+ "htaccess",
+ "http",
+ "security"
+ ]
+ },
+ "app=core&module=overview&controller=security&do=settings&searchResult=form_match_ipaddress": {
+ "lang_key": "match_ipaddress",
+ "restriction": "security_manage",
+ "keywords": [
+ "ip address",
+ "session"
+ ]
+ },
+ "app=core&module=overview&controller=security&do=settings&searchResult=form_security_remove_acp_link": {
+ "lang_key": "security_remove_acp_link",
+ "restriction": "security_manage",
+ "keywords": [
+ "acp",
+ "admin cp",
+ "admincp"
+ ]
+ },
+ "app=core&module=overview&controller=security&do=settings&searchResult=form_xforward_matching": {
+ "lang_key": "xforward_matching",
+ "restriction": "security_manage",
+ "keywords": [
+ "http_x_forwarded_for",
+ "ip address",
+ "proxies",
+ "proxy"
+ ]
+ },
+ "app=core&module=overview&controller=security&do=settings&searchResult=password_strength_meter": {
+ "lang_key": "password_strength_meter",
+ "restriction": "security_manage",
+ "keywords": [
+ "meter",
+ "password",
+ "strength"
+ ]
+ },
+ "app=core&module=promotion&controller=advertisements": {
+ "lang_key": "menu__core_promotion_advertisements",
+ "restriction": "advertisements_manage",
+ "keywords": [
+ "ads",
+ "adsense",
+ "advertisements",
+ "adverts",
+ "adwords",
+ "google"
+ ]
+ },
+ "app=core&module=promotion&controller=analytics": {
+ "lang_key": "ipbseo_ga_enabled",
+ "restriction": "analytics_manage",
+ "keywords": [
+ "analytics",
+ "ga",
+ "google",
+ "google analytics",
+ "omniture",
+ "piwik",
+ "tracking"
+ ]
+ },
+ "app=core&module=promotion&controller=seo": {
+ "lang_key": "menu__core_promotion_seo",
+ "restriction": "seo_manage",
+ "keywords": [
+ "analytics",
+ "furl",
+ "search",
+ "search engine optimization",
+ "seo"
+ ]
+ },
+ "app=core&module=promotion&controller=seo&tab=metatags": {
+ "lang_key": "seo_tab_metatags",
+ "restriction": "seo_manage",
+ "keywords": [
+ "description",
+ "keywords",
+ "meta tags",
+ "robots"
+ ]
+ },
+ "app=core&module=promotion&controller=seo&tab=sitemap": {
+ "lang_key": "seo_tab_sitemap",
+ "restriction": "seo_manage",
+ "keywords": [
+ "google",
+ "priority",
+ "site map",
+ "sitemap",
+ "xml"
+ ]
+ },
+ "app=core&module=promotion&controller=seo&tab=urls": {
+ "lang_key": "seo_tab_furls",
+ "restriction": "seo_manage",
+ "keywords": [
+ "friendly url",
+ "furls",
+ "htaccess",
+ "mod_rewrite",
+ "urls"
+ ]
+ },
+ "app=core&module=promotion&controller=sharelinks": {
+ "lang_key": "menu__core_promotion_sharelinks",
+ "restriction": "sharelinks_manage",
+ "keywords": [
+ "delicious",
+ "digg",
+ "facebook",
+ "google",
+ "linkedin",
+ "reddit",
+ "share links",
+ "stumbleupon",
+ "twitter"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&do=tasks": {
+ "lang_key": "task_manager",
+ "restriction": null,
+ "keywords": [
+ "cron",
+ "cronjob",
+ "task",
+ "tasklog",
+ "tasks"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&searchResult=form_auto_polling_enabled": {
+ "lang_key": "auto_polling_enabled",
+ "restriction": "advanced_manage",
+ "keywords": [
+ "auto",
+ "automatic",
+ "polling"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&tab=datastore": {
+ "lang_key": "data_store",
+ "restriction": "datastore",
+ "keywords": [
+ "storage"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&tab=datastore&searchResult=cache_guest_page": {
+ "lang_key": "cache_guest_page",
+ "restriction": "advanced_manage_server",
+ "keywords": [
+ "cache",
+ "guest cache",
+ "output cache"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&tab=datastore&searchResult=cache_method": {
+ "lang_key": "cache_method",
+ "restriction": "datastore",
+ "keywords": [
+ "apc",
+ "memcached",
+ "redis",
+ "wincache",
+ "xcache"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&tab=furl": {
+ "lang_key": "furls_customize",
+ "restriction": "advanced_manage_furls",
+ "keywords": [
+ "friendly",
+ "furl",
+ "seo"
+ ]
+ },
+ "app=core&module=settings&controller=advanced&tab=settings&searchResult=task_use_cron": {
+ "lang_key": "task_use_cron",
+ "restriction": "advanced_manage_server",
+ "keywords": [
+ "cron job",
+ "cronjob",
+ "tasks"
+ ]
+ },
+ "app=core&module=settings&controller=contactus": {
+ "lang_key": "menu__core_settings_contactus",
+ "restriction": "contactus_manage",
+ "keywords": [
+ "contact us"
+ ]
+ },
+ "app=core&module=settings&controller=email": {
+ "lang_key": "menu__core_settings_email",
+ "restriction": "email_manage",
+ "keywords": [
+ "address",
+ "email",
+ "email logo",
+ "mail",
+ "smtp"
+ ]
+ },
+ "app=core&module=settings&controller=email&do=errorLog": {
+ "lang_key": "emailerrorlogs",
+ "restriction": "email_errorlog",
+ "keywords": [
+ "email error logs",
+ "email logs",
+ "logs"
+ ]
+ },
+ "app=core&module=settings&controller=email&searchResult=settings_form_email_color": {
+ "lang_key": "email_color",
+ "restriction": "email_manage",
+ "keywords": [
+ "email color",
+ "email colour"
+ ]
+ },
+ "app=core&module=settings&controller=email&searchResult=settings_form_email_in": {
+ "lang_key": "email_in",
+ "restriction": "email_manage",
+ "keywords": [
+ "contact email",
+ "contact us email",
+ "incoming email"
+ ]
+ },
+ "app=core&module=settings&controller=email&searchResult=settings_form_email_logo": {
+ "lang_key": "email_logo",
+ "restriction": "email_manage",
+ "keywords": [
+ "email logo"
+ ]
+ },
+ "app=core&module=settings&controller=email&searchResult=settings_form_email_out": {
+ "lang_key": "email_out",
+ "restriction": "email_manage",
+ "keywords": [
+ "email",
+ "outgoing email",
+ "site email"
+ ]
+ },
+ "app=core&module=settings&controller=email&searchResult=settings_form_mail_method": {
+ "lang_key": "mail_method",
+ "restriction": "email_manage",
+ "keywords": [
+ "email",
+ "mail",
+ "smtp"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_board_name": {
+ "lang_key": "board_name",
+ "restriction": "general_manage",
+ "keywords": [
+ "community name",
+ "site name",
+ "website name"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_copyright_line": {
+ "lang_key": "copyright_line",
+ "restriction": "general_manage",
+ "keywords": [
+ "copyright",
+ "license"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_show_home_link": {
+ "lang_key": "show_home_link",
+ "restriction": "general_manage",
+ "keywords": [
+ "breadcrumb",
+ "navigation"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_site_address": {
+ "lang_key": "site_address",
+ "restriction": "general_manage",
+ "keywords": [
+ "address",
+ "mail"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_site_online": {
+ "lang_key": "site_online",
+ "restriction": "general_manage",
+ "keywords": [
+ "offline",
+ "online"
+ ]
+ },
+ "app=core&module=settings&controller=general&searchResult=form_upgrade_email": {
+ "lang_key": "upgrade_email",
+ "restriction": "general_manage",
+ "keywords": [
+ "update",
+ "upgrade"
+ ]
+ },
+ "app=core&module=settings&controller=licensekey": {
+ "lang_key": "menu__core_settings_licensekey",
+ "restriction": "licensekey_manage",
+ "keywords": [
+ "license key"
+ ]
+ },
+ "app=core&module=settings&controller=login": {
+ "lang_key": "menu__core_settings_login",
+ "restriction": "login_manage",
+ "keywords": [
+ "accounts",
+ "facebook",
+ "google",
+ "https",
+ "ips connect",
+ "ldap",
+ "locking",
+ "login",
+ "microsoft",
+ "twitter",
+ "windows"
+ ]
+ },
+ "app=core&module=settings&controller=login&do=settings&searchResult=disable_anonymous": {
+ "lang_key": "disable_anonymous",
+ "restriction": "login_manage",
+ "keywords": [
+ "anonymous",
+ "anonymous login"
+ ]
+ },
+ "app=core&module=settings&controller=login&do=settings&searchResult=form_logins_over_https": {
+ "lang_key": "logins_over_https",
+ "restriction": "login_manage",
+ "keywords": [
+ "https",
+ "secure logins",
+ "ssl"
+ ]
+ },
+ "app=core&module=settings&controller=login&do=settings&searchResult=ipb_bruteforce_attempts": {
+ "lang_key": "ipb_bruteforce_attempts",
+ "restriction": "login_manage",
+ "keywords": [
+ "brute",
+ "bruteforce",
+ "failed",
+ "logins"
+ ]
+ },
+ "app=core&module=settings&controller=mfa": {
+ "lang_key": "menu__core_settings_mfa",
+ "restriction": "mfa_manage",
+ "keywords": [
+ "2 factor authentication",
+ "2 factor verification",
+ "2-factor authentication",
+ "2-factor verification",
+ "2fa",
+ "account authentication",
+ "account verification",
+ "authentication",
+ "google authentication",
+ "google authenticator",
+ "mfa",
+ "multi factor authentication",
+ "multi factor verification",
+ "multi-factor authentication",
+ "multi-factor verification",
+ "multifactor authentication",
+ "multifactor verification",
+ "security",
+ "security questions",
+ "two factor authentication",
+ "two factor verification",
+ "two-factor authentication",
+ "two-factor verification",
+ "verification"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=attachment_image_size": {
+ "lang_key": "attachment_image_size",
+ "restriction": "posting_manage",
+ "keywords": [
+ "thumbnail dimensions",
+ "thumbnail size"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=attachment_resample_size": {
+ "lang_key": "attachment_resample_size",
+ "restriction": "posting_manage",
+ "keywords": [
+ "image dimensions",
+ "image size"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_allow_remote_images": {
+ "lang_key": "allow_remote_images",
+ "restriction": "posting_manage",
+ "keywords": [
+ "external images",
+ "linked images",
+ "remote images"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_attach_allowed_types": {
+ "lang_key": "attach_allowed_types",
+ "restriction": null,
+ "keywords": [
+ "attachment restrictions",
+ "attachments",
+ "file types",
+ "files"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_edit_log": {
+ "lang_key": "edit_log",
+ "restriction": "posting_manage",
+ "keywords": [
+ "edit log",
+ "editing"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_editor_embeds": {
+ "lang_key": "editor_embeds",
+ "restriction": "posting_manage",
+ "keywords": [
+ "disable",
+ "embed",
+ "embeds",
+ "facebook",
+ "youtube"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_flood_control": {
+ "lang_key": "flood_control",
+ "restriction": "posting_manage",
+ "keywords": [
+ "flood control"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_max_title_length": {
+ "lang_key": "max_title_length",
+ "restriction": "posting_manage",
+ "keywords": [
+ "title length",
+ "title size"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_max_video_width": {
+ "lang_key": "max_video_width",
+ "restriction": "posting_manage",
+ "keywords": [
+ "video size",
+ "video width",
+ "videos"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_merge_concurrent_posts": {
+ "lang_key": "merge_concurrent_posts",
+ "restriction": null,
+ "keywords": [
+ "concurrent",
+ "merge",
+ "merging"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_reviews_rating_out_of": {
+ "lang_key": "reviews_rating_out_of",
+ "restriction": "posting_manage",
+ "keywords": [
+ "reviews",
+ "stars"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=form_topic_redirect_prune": {
+ "lang_key": "topic_redirect_prune",
+ "restriction": "posting_manage",
+ "keywords": [
+ "moved links",
+ "shadow topics"
+ ]
+ },
+ "app=core&module=settings&controller=posting&searchResult=remote_image_proxy": {
+ "lang_key": "remote_image_proxy",
+ "restriction": "posting_manage",
+ "keywords": [
+ "external image proxy",
+ "https",
+ "image proxy",
+ "proxy",
+ "remote image proxy",
+ "ssl"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=acronymExpansion": {
+ "lang_key": "acronym_expansion",
+ "restriction": "posting_manage_acronym",
+ "keywords": [
+ "abbreviations",
+ "acronyms"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=attachment_image_size": {
+ "lang_key": "attachment_image_size",
+ "restriction": null,
+ "keywords": [
+ "attachments",
+ "images",
+ "thumbnails",
+ "uploads"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_allow_remote_images": {
+ "lang_key": "allow_remote_images",
+ "restriction": null,
+ "keywords": [
+ "external",
+ "images",
+ "linked",
+ "remote"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_attach_allowed_types": {
+ "lang_key": "attach_allowed_types",
+ "restriction": null,
+ "keywords": [
+ "attachments",
+ "extensions",
+ "uploads"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_edit_log": {
+ "lang_key": "edit_log",
+ "restriction": null,
+ "keywords": [
+ "edit",
+ "log"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_flood_control": {
+ "lang_key": "flood_control",
+ "restriction": null,
+ "keywords": [
+ "flood",
+ "limit",
+ "posting"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_max_title_length": {
+ "lang_key": "max_title_length",
+ "restriction": null,
+ "keywords": [
+ "titles"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_max_video_width": {
+ "lang_key": "max_video_width",
+ "restriction": null,
+ "keywords": [
+ "embedded",
+ "media",
+ "videos",
+ "vimeo",
+ "youtube"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_merge_concurrent_posts": {
+ "lang_key": "merge_concurrent_posts",
+ "restriction": null,
+ "keywords": [
+ "combine",
+ "concurrent",
+ "merge"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_reviews_rating_out_of": {
+ "lang_key": "reviews_rating_out_of",
+ "restriction": null,
+ "keywords": [
+ "ratings",
+ "reviews",
+ "stars"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=general&searchResult=form_topic_redirect_prune": {
+ "lang_key": "topic_redirect_prune",
+ "restriction": null,
+ "keywords": [
+ "moved",
+ "shadow"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=polls": {
+ "lang_key": "polls",
+ "restriction": "posting_manage_polls",
+ "keywords": [
+ "polls",
+ "question",
+ "vote"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=profanityFilters": {
+ "lang_key": "profanity",
+ "restriction": "posting_manage_profanity",
+ "keywords": [
+ "bad words",
+ "profanity",
+ "swear words"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=tags": {
+ "lang_key": "tags",
+ "restriction": "posting_manage_tags",
+ "keywords": [
+ "prefixes",
+ "tagging",
+ "tags"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=urlFilters": {
+ "lang_key": "ipb_url_filter_option",
+ "restriction": "posting_manage_url",
+ "keywords": [
+ "blacklist",
+ "links",
+ "urls",
+ "whitelist"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=form_links_external": {
+ "lang_key": "links_external",
+ "restriction": "posting_manage",
+ "keywords": [
+ "blank",
+ "link",
+ "tab",
+ "target",
+ "window"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=ipb_url_blacklist": {
+ "lang_key": "ipb_url_blacklist",
+ "restriction": "posting_manage",
+ "keywords": [
+ "blacklist"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=posts_add_nofollow": {
+ "lang_key": "posts_add_nofollow",
+ "restriction": "posting_manage",
+ "keywords": [
+ "nofollow",
+ "pagerank"
+ ]
+ },
+ "app=core&module=settings&controller=posting&tab=urlFilters&searchResult=posts_add_nofollow_exclude": {
+ "lang_key": "posts_add_nofollow_exclude",
+ "restriction": null,
+ "keywords": [
+ "follow",
+ "nofollow",
+ "pagerank"
+ ]
+ },
+ "app=core&module=settings&controller=securityquestions": {
+ "lang_key": "menu__core_settings_securityquestions",
+ "restriction": "securityquestions_manage",
+ "keywords": [
+ "answers",
+ "questions",
+ "security"
+ ]
+ },
+ "app=core&module=settings&controller=terms": {
+ "lang_key": "menu__core_settings_terms",
+ "restriction": "terms_manage",
+ "keywords": [
+ "conditions",
+ "guidelines",
+ "legal",
+ "privacy policy",
+ "rules",
+ "terms"
+ ]
+ },
+ "app=core&module=settings&controller=terms&searchResult=form_guest_terms_bar": {
+ "lang_key": "guest_terms_bar",
+ "restriction": "terms_manage",
+ "keywords": [
+ "terms bar"
+ ]
+ },
+ "app=core&module=staff&controller=admin": {
+ "lang_key": "menu__core_staff_admin",
+ "restriction": "restrictions_manage",
+ "keywords": [
+ "acp",
+ "administrators",
+ "admins",
+ "restrictions",
+ "staff"
+ ]
+ },
+ "app=core&module=staff&controller=admin&do=actionLogs": {
+ "lang_key": "acplogs",
+ "restriction": "restrictions_adminlogs",
+ "keywords": [
+ "acp logs",
+ "admin logs",
+ "logs"
+ ]
+ },
+ "app=core&module=staff&controller=admin&do=loginLogs": {
+ "lang_key": "menu__core_staff_admin",
+ "restriction": "restrictions_manage",
+ "keywords": [
+ "acp logins",
+ "admin logins"
+ ]
+ },
+ "app=core&module=staff&controller=directory": {
+ "lang_key": "menu__core_staff_directory",
+ "restriction": "leaders_manage",
+ "keywords": [
+ "leaders",
+ "moderators",
+ "staff"
+ ]
+ },
+ "app=core&module=staff&controller=moderators": {
+ "lang_key": "menu__core_staff_moderators",
+ "restriction": "moderators_manage",
+ "keywords": [
+ "moderators",
+ "staff"
+ ]
+ },
+ "app=core&module=staff&controller=moderators&do=actionLogs": {
+ "lang_key": "modlogs",
+ "restriction": "restrictions_moderatorlogs",
+ "keywords": [
+ "logs",
+ "mod logs",
+ "moderator logs",
+ "staff logs"
+ ]
+ },
+ "app=core&module=stats&controller=pmstats": {
+ "lang_key": "menu__core_members_pmstats",
+ "restriction": null,
+ "keywords": [
+ "message statistics",
+ "Messenger Statistics",
+ "private messages"
+ ]
+ },
+ "app=core&module=stats&controller=registrationstats": {
+ "lang_key": "menu__core_members_registrationstats",
+ "restriction": null,
+ "keywords": [
+ "registration statistics",
+ "Registrations"
+ ]
+ },
+ "app=core&module=support&controller=errorLogs": {
+ "lang_key": "error_logs",
+ "restriction": null,
+ "keywords": [
+ "error log",
+ "errors",
+ "logs"
+ ]
+ },
+ "app=core&module=support&controller=sql": {
+ "lang_key": "sql_toolbox",
+ "restriction": "sql_toolbox",
+ "keywords": [
+ "sql toolbox"
+ ]
+ },
+ "app=core&module=support&controller=support": {
+ "lang_key": "menu__core_support_dashboard",
+ "restriction": "support_dashboard",
+ "keywords": [
+ "diagnose",
+ "diagnostics",
+ "documentation",
+ "help",
+ "phpinfo",
+ "server",
+ "support",
+ "system",
+ "tech support",
+ "technical support",
+ "troubleshoot"
+ ]
+ },
+ "app=core&module=support&controller=systemLogs": {
+ "lang_key": "system_logs",
+ "restriction": null,
+ "keywords": [
+ "driver errors",
+ "logs",
+ "sql error logs",
+ "sql logs",
+ "system logs"
+ ]
+ }
}
\ No newline at end of file
diff --git a/applications/core/data/javascript.xml b/applications/core/data/javascript.xml
index 34762834..514ffa27 100644
--- a/applications/core/data/javascript.xml
+++ b/applications/core/data/javascript.xml
@@ -1,6 +1,6 @@
-
-
-
-
-
-
- /* global ips, _, Debug */
+ /* global ips, _, Debug */
/**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
@@ -2333,7 +2333,7 @@ var Debug = Debug || {};
};
}(jQuery, _));
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -5013,7 +5013,7 @@ function recaptcha2Callback(){
};
});
}(jQuery, _));
-
-
-
-
- /* global ips, _ */
+ /* global ips, _ */
/**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
@@ -8405,7 +8405,7 @@ function recaptcha2Callback(){
};
};
}(jQuery, _));
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -16543,7 +16543,7 @@ function recaptcha2Callback(){
};
}(jQuery, _));
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -21781,7 +21801,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -22846,7 +22866,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -23169,7 +23189,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -26079,7 +26099,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -26632,7 +26652,7 @@ var escapeRegExp;
});
}(jQuery, _));
- /**
+ /**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -26687,7 +26707,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
- /**
+ /**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -27523,7 +27543,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -28197,7 +28217,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -28841,7 +28861,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -28923,7 +28943,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
-
-
- /**
+ /**
* IPS Social Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -30071,7 +30091,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -30679,7 +30699,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -31440,7 +31460,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -32174,7 +32194,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -32528,7 +32548,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -34034,7 +34054,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2015 Invision Power Services - http://www.invisionpower.com
*
@@ -34493,7 +34513,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -34700,7 +34720,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -35161,7 +35181,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -35207,7 +35227,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37155,7 +37175,7 @@ var escapeRegExp;
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37253,7 +37273,7 @@ var escapeRegExp;
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37297,7 +37317,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37336,7 +37356,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37515,7 +37535,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37589,7 +37609,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -37964,7 +37984,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2016 Invision Power Services - http://www.invisionpower.com
*
@@ -41288,7 +41308,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2016 Invision Power Services - http://www.invisionpower.com
*
@@ -41314,7 +41334,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -41676,7 +41696,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -41806,7 +41826,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -42525,7 +42545,7 @@ var escapeRegExp;
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -42610,7 +42630,7 @@ var escapeRegExp;
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -42658,7 +42678,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -42756,7 +42776,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2015 Invision Power Services - http://www.invisionpower.com
*
@@ -42834,7 +42854,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -43066,7 +43086,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -43149,7 +43169,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -44536,7 +44556,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
- /**
+ /**
* IPS Community Suite 4
* (c) 2013 Invision Power Services - http://www.invisionpower.com
*
@@ -46219,7 +46239,7 @@ var escapeRegExp;
}
});
}(jQuery, _));
-
-
-
-
-
- i;i++){var l=r.eq(i);if(t.append(l),n&&t.append(n),a(t,e)){l.remove(),o=!0;break}n&&n.detach()}return o}function r(e,n,i,d,l){var s=!1,c="a, table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style",u="script, .dotdotdot-keep";return e.contents().detach().each(function(){var h=this,f=t(h);if("undefined"==typeof h)return!0;if(f.is(u))e.append(f);else{if(s)return!0;e.append(f),!l||f.is(d.after)||f.find(d.after).length||e[e.is(c)?"after":"append"](l),a(i,d)&&(s=3==h.nodeType?o(f,n,i,d,l):r(f,n,i,d,l)),s||l&&l.detach()}}),n.addClass("is-truncated"),s}function o(e,n,r,o,d){var c=e[0];if(!c)return!1;var h=s(c),f=-1!==h.indexOf(" ")?" ":" ",p="letter"==o.wrap?"":f,g=h.split(p),v=-1,w=-1,b=0,m=g.length-1;for(o.fallbackToLetter&&0==b&&0==m&&(p="",g=h.split(p),m=g.length-1);m>=b&&(0!=b||0!=m);){var y=Math.floor((b+m)/2);if(y==w)break;w=y,l(c,g.slice(0,w+1).join(p)+o.ellipsis),r.children().each(function(){t(this).toggle().toggle()}),a(r,o)?(m=w,o.fallbackToLetter&&0==b&&0==m&&(p="",g=g[0].split(p),v=-1,w=-1,b=0,m=g.length-1)):(v=w,b=w)}if(-1==v||1==g.length&&0==g[0].length){var x=e.parent();e.detach();var C=d&&d.closest(x).length?d.length:0;if(x.contents().length>C?c=u(x.contents().eq(-1-C),n):(c=u(x,n,!0),C||x.detach()),c&&(h=i(s(c),o),l(c,h),C&&d)){var T=d.parent();t(c).parent().append(d),t.trim(T.html())||T.remove()}}else h=i(g.slice(0,v+1).join(p),o),l(c,h);return!0}function a(t,e){return t.innerHeight()>e.maxHeight}function i(e,n){for(;t.inArray(e.slice(-1),n.lastCharacter.remove)>-1;)e=e.slice(0,-1);return t.inArray(e.slice(-1),n.lastCharacter.noEllipsis)<0&&(e+=n.ellipsis),e}function d(t){return{width:t.innerWidth(),height:t.innerHeight()}}function l(t,e){t.innerText?t.innerText=e:t.nodeValue?t.nodeValue=e:t.textContent&&(t.textContent=e)}function s(t){return t.innerText?t.innerText:t.nodeValue?t.nodeValue:t.textContent?t.textContent:""}function c(t){do t=t.previousSibling;while(t&&1!==t.nodeType&&3!==t.nodeType);return t}function u(e,n,r){var o,a=e&&e[0];if(a){if(!r){if(3===a.nodeType)return a;if(t.trim(e.text()))return u(e.contents().last(),n)}for(o=c(a);!o;){if(e=e.parent(),e.is(n)||!e.length)return!1;o=c(e[0])}if(o)return u(t(o),n)}return!1}function h(e,n){return e?"string"==typeof e?(e=t(e,n),e.length?e:!1):e.jquery?e:!1:!1}function f(t){for(var e=t.innerHeight(),n=["paddingTop","paddingBottom"],r=0,o=n.length;o>r;r++){var a=parseInt(t.css(n[r]),10);isNaN(a)&&(a=0),e-=a}return e}if(!t.fn.dotdotdot){t.fn.dotdotdot=function(e){if(0==this.length)return t.fn.dotdotdot.debug('No element found for "'+this.selector+'".'),this;if(this.length>1)return this.each(function(){t(this).dotdotdot(e)});var o=this,i=o.contents();o.data("dotdotdot")&&o.trigger("destroy.dot"),o.data("dotdotdot-style",o.attr("style")||""),o.css("word-wrap","break-word"),"nowrap"===o.css("white-space")&&o.css("white-space","normal"),o.bind_events=function(){return o.bind("update.dot",function(e,d){switch(o.removeClass("is-truncated"),e.preventDefault(),e.stopPropagation(),typeof l.height){case"number":l.maxHeight=l.height;break;case"function":l.maxHeight=l.height.call(o[0]);break;default:l.maxHeight=f(o)}l.maxHeight+=l.tolerance,"undefined"!=typeof d&&(("string"==typeof d||"nodeType"in d&&1===d.nodeType)&&(d=t("
").append(d).contents()),d instanceof t&&(i=d)),g=o.wrapInner('
').children(),g.contents().detach().end().append(i.clone(!0)).find("br").replaceWith(" ").end().css({height:"auto",width:"auto",border:"none",padding:0,margin:0});var c=!1,u=!1;return s.afterElement&&(c=s.afterElement.clone(!0),c.show(),s.afterElement.detach()),a(g,l)&&(u="children"==l.wrap?n(g,l,c):r(g,o,g,l,c)),g.replaceWith(g.contents()),g=null,t.isFunction(l.callback)&&l.callback.call(o[0],u,i),s.isTruncated=u,u}).bind("isTruncated.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],s.isTruncated),s.isTruncated}).bind("originalContent.dot",function(t,e){return t.preventDefault(),t.stopPropagation(),"function"==typeof e&&e.call(o[0],i),i}).bind("destroy.dot",function(t){t.preventDefault(),t.stopPropagation(),o.unwatch().unbind_events().contents().detach().end().append(i).attr("style",o.data("dotdotdot-style")||"").removeClass("is-truncated").data("dotdotdot",!1)}),o},o.unbind_events=function(){return o.unbind(".dot"),o},o.watch=function(){if(o.unwatch(),"window"==l.watch){var e=t(window),n=e.width(),r=e.height();e.bind("resize.dot"+s.dotId,function(){n==e.width()&&r==e.height()&&l.windowResizeFix||(n=e.width(),r=e.height(),u&&clearInterval(u),u=setTimeout(function(){o.trigger("update.dot")},100))})}else c=d(o),u=setInterval(function(){if(o.is(":visible")){var t=d(o);c.width==t.width&&c.height==t.height||(o.trigger("update.dot"),c=t)}},500);return o},o.unwatch=function(){return t(window).unbind("resize.dot"+s.dotId),u&&clearInterval(u),o};var l=t.extend(!0,{},t.fn.dotdotdot.defaults,e),s={},c={},u=null,g=null;return l.lastCharacter.remove instanceof Array||(l.lastCharacter.remove=t.fn.dotdotdot.defaultArrays.lastCharacter.remove),l.lastCharacter.noEllipsis instanceof Array||(l.lastCharacter.noEllipsis=t.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis),s.afterElement=h(l.after,o),s.isTruncated=!1,s.dotId=p++,o.data("dotdotdot",!0).bind_events().trigger("update.dot"),l.watch&&o.watch(),o},t.fn.dotdotdot.defaults={ellipsis:"... ",wrap:"word",fallbackToLetter:!0,lastCharacter:{},tolerance:0,callback:null,after:null,height:null,watch:!1,windowResizeFix:!0},t.fn.dotdotdot.defaultArrays={lastCharacter:{remove:[" "," ",",",";",".","!","?"],noEllipsis:[]}},t.fn.dotdotdot.debug=function(t){};var p=1,g=t.fn.html;t.fn.html=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?this.trigger("update",[n]):g.apply(this,arguments)};var v=t.fn.text;t.fn.text=function(n){return n!=e&&!t.isFunction(n)&&this.data("dotdotdot")?(n=t("
").text(n).html(),this.trigger("update",[n])):v.apply(this,arguments)}}}(jQuery),jQuery(document).ready(function(t){t(".dot-ellipsis").each(function(){var e=t(this).hasClass("dot-resize-update"),n=t(this).hasClass("dot-timer-update"),r=0,o=t(this).attr("class").split(/\s+/);t.each(o,function(t,e){var n=e.match(/^dot-height-(\d+)$/);null!==n&&(r=Number(n[1]))});var a=new Object;n&&(a.watch=!0),e&&(a.watch="window"),r>0&&(a.height=r),t(this).dotdotdot(a)})}),jQuery(window).on("load",function(){jQuery(".dot-ellipsis.dot-load-update").trigger("update.dot")});]]>
- ")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)]]>
- ")&&n[0]);return e>4?e:!1}();return e},h.isInternetExplorer=function(){var e=h.isInternetExplorer.cached=typeof h.isInternetExplorer.cached!="undefined"?h.isInternetExplorer.cached:Boolean(h.getInternetExplorerMajorVersion());return e},h.options.html4Mode?h.emulated={pushState:!0,hashChange:!0}:h.emulated={pushState:!Boolean(e.history&&e.history.pushState&&e.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(i.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(i.userAgent)),hashChange:Boolean(!("onhashchange"in e||"onhashchange"in r)||h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8)},h.enabled=!h.emulated.pushState,h.bugs={setHash:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),safariPoll:Boolean(!h.emulated.pushState&&i.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(i.userAgent)),ieDoubleCheck:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(h.isInternetExplorer()&&h.getInternetExplorerMajorVersion()<7)},h.isEmptyObject=function(e){for(var t in e)if(e.hasOwnProperty(t))return!1;return!0},h.cloneObject=function(e){var t,n;return e?(t=l.stringify(e),n=l.parse(t)):n={},n},h.getRootUrl=function(){var e=r.location.protocol+"//"+(r.location.hostname||r.location.host);if(r.location.port||!1)e+=":"+r.location.port;return e+="/",e},h.getBaseHref=function(){var e=r.getElementsByTagName("base"),t=null,n="";return e.length===1&&(t=e[0],n=t.href.replace(/[^\/]+$/,"")),n=n.replace(/\/+$/,""),n&&(n+="/"),n},h.getBaseUrl=function(){var e=h.getBaseHref()||h.getBasePageUrl()||h.getRootUrl();return e},h.getPageUrl=function(){var e=h.getState(!1,!1),t=(e||{}).url||h.getLocationHref(),n;return n=t.replace(/\/+$/,"").replace(/[^\/]+$/,function(e,t,n){return/\./.test(e)?e:e+"/"}),n},h.getBasePageUrl=function(){var e=h.getLocationHref().replace(/[#\?].*/,"").replace(/[^\/]+$/,function(e,t,n){return/[^\/]$/.test(e)?"":e}).replace(/\/+$/,"")+"/";return e},h.getFullUrl=function(e,t){var n=e,r=e.substring(0,1);return t=typeof t=="undefined"?!0:t,/[a-z]+\:\/\//.test(e)||(r==="/"?n=h.getRootUrl()+e.replace(/^\/+/,""):r==="#"?n=h.getPageUrl().replace(/#.*/,"")+e:r==="?"?n=h.getPageUrl().replace(/[\?#].*/,"")+e:t?n=h.getBaseUrl()+e.replace(/^(\.\/)+/,""):n=h.getBasePageUrl()+e.replace(/^(\.\/)+/,"")),n.replace(/\#$/,"")},h.getShortUrl=function(e){var t=e,n=h.getBaseUrl(),r=h.getRootUrl();return h.emulated.pushState&&(t=t.replace(n,"")),t=t.replace(r,"/"),h.isTraditionalAnchor(t)&&(t="./"+t),t=t.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),t},h.getLocationHref=function(e){return e=e||r,e.URL===e.location.href?e.location.href:e.location.href===decodeURIComponent(e.URL)?e.URL:e.location.hash&&decodeURIComponent(e.location.href.replace(/^[^#]+/,""))===e.location.hash?e.location.href:e.URL.indexOf("#")==-1&&e.location.href.indexOf("#")!=-1?e.location.href:e.URL||e.location.href},h.store={},h.idToState=h.idToState||{},h.stateToId=h.stateToId||{},h.urlToId=h.urlToId||{},h.storedStates=h.storedStates||[],h.savedStates=h.savedStates||[],h.normalizeStore=function(){h.store.idToState=h.store.idToState||{},h.store.urlToId=h.store.urlToId||{},h.store.stateToId=h.store.stateToId||{}},h.getState=function(e,t){typeof e=="undefined"&&(e=!0),typeof t=="undefined"&&(t=!0);var n=h.getLastSavedState();return!n&&t&&(n=h.createStateObject()),e&&(n=h.cloneObject(n),n.url=n.cleanUrl||n.url),n},h.getIdByState=function(e){var t=h.extractId(e.url),n;if(!t){n=h.getStateString(e);if(typeof h.stateToId[n]!="undefined")t=h.stateToId[n];else if(typeof h.store.stateToId[n]!="undefined")t=h.store.stateToId[n];else{for(;;){t=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof h.idToState[t]=="undefined"&&typeof h.store.idToState[t]=="undefined")break}h.stateToId[n]=t,h.idToState[t]=e}}return t},h.normalizeState=function(e){var t,n;if(!e||typeof e!="object")e={};if(typeof e.normalized!="undefined")return e;if(!e.data||typeof e.data!="object")e.data={};return t={},t.normalized=!0,t.title=e.title||"",t.url=h.getFullUrl(e.url?e.url:h.getLocationHref()),t.hash=h.getShortUrl(t.url),t.data=h.cloneObject(e.data),t.id=h.getIdByState(t),t.cleanUrl=t.url.replace(/\??\&_suid.*/,""),t.url=t.cleanUrl,n=!h.isEmptyObject(t.data),(t.title||n)&&h.options.disableSuid!==!0&&(t.hash=h.getShortUrl(t.url).replace(/\??\&_suid.*/,""),/\?/.test(t.hash)||(t.hash+="?"),t.hash+="&_suid="+t.id),t.hashedUrl=h.getFullUrl(t.hash),(h.emulated.pushState||h.bugs.safariPoll)&&h.hasUrlDuplicate(t)&&(t.url=t.hashedUrl),t},h.createStateObject=function(e,t,n){var r={data:e,title:t,url:n};return r=h.normalizeState(r),r},h.getStateById=function(e){e=String(e);var n=h.idToState[e]||h.store.idToState[e]||t;return n},h.getStateString=function(e){var t,n,r;return t=h.normalizeState(e),n={data:t.data,title:e.title,url:e.url},r=l.stringify(n),r},h.getStateId=function(e){var t,n;return t=h.normalizeState(e),n=t.id,n},h.getHashByState=function(e){var t,n;return t=h.normalizeState(e),n=t.hash,n},h.extractId=function(e){var t,n,r,i;return e.indexOf("#")!=-1?i=e.split("#")[0]:i=e,n=/(.*)\&_suid=([0-9]+)$/.exec(i),r=n?n[1]||e:e,t=n?String(n[2]||""):"",t||!1},h.isTraditionalAnchor=function(e){var t=!/[\/\?\.]/.test(e);return t},h.extractState=function(e,t){var n=null,r,i;return t=t||!1,r=h.extractId(e),r&&(n=h.getStateById(r)),n||(i=h.getFullUrl(e),r=h.getIdByUrl(i)||!1,r&&(n=h.getStateById(r)),!n&&t&&!h.isTraditionalAnchor(e)&&(n=h.createStateObject(null,null,i))),n},h.getIdByUrl=function(e){var n=h.urlToId[e]||h.store.urlToId[e]||t;return n},h.getLastSavedState=function(){return h.savedStates[h.savedStates.length-1]||t},h.getLastStoredState=function(){return h.storedStates[h.storedStates.length-1]||t},h.hasUrlDuplicate=function(e){var t=!1,n;return n=h.extractState(e.url),t=n&&n.id!==e.id,t},h.storeState=function(e){return h.urlToId[e.url]=e.id,h.storedStates.push(h.cloneObject(e)),e},h.isLastSavedState=function(e){var t=!1,n,r,i;return h.savedStates.length&&(n=e.id,r=h.getLastSavedState(),i=r.id,t=n===i),t},h.saveState=function(e){return h.isLastSavedState(e)?!1:(h.savedStates.push(h.cloneObject(e)),!0)},h.getStateByIndex=function(e){var t=null;return typeof e=="undefined"?t=h.savedStates[h.savedStates.length-1]:e<0?t=h.savedStates[h.savedStates.length+e]:t=h.savedStates[e],t},h.getCurrentIndex=function(){var e=null;return h.savedStates.length<1?e=0:e=h.savedStates.length-1,e},h.getHash=function(e){var t=h.getLocationHref(e),n;return n=h.getHashByUrl(t),n},h.unescapeHash=function(e){var t=h.normalizeHash(e);return t=decodeURIComponent(t),t},h.normalizeHash=function(e){var t=e.replace(/[^#]*#/,"").replace(/#.*/,"");return t},h.setHash=function(e,t){var n,i;return t!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.setHash,args:arguments,queue:t}),!1):(h.busy(!0),n=h.extractState(e,!0),n&&!h.emulated.pushState?h.pushState(n.data,n.title,n.url,!1):h.getHash()!==e&&(h.bugs.setHash?(i=h.getPageUrl(),h.pushState(null,null,i+"#"+e,!1)):r.location.hash=e),h)},h.escapeHash=function(t){var n=h.normalizeHash(t);return n=e.encodeURIComponent(n),h.bugs.hashEscape||(n=n.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),n},h.getHashByUrl=function(e){var t=String(e).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return t=h.unescapeHash(t),t},h.setTitle=function(e){var t=e.title,n;t||(n=h.getStateByIndex(0),n&&n.url===e.url&&(t=n.title||h.options.initialTitle));try{r.getElementsByTagName("title")[0].innerHTML=t.replace("<","<").replace(">",">").replace(" & "," & ")}catch(i){}return r.title=t,h},h.queues=[],h.busy=function(e){typeof e!="undefined"?h.busy.flag=e:typeof h.busy.flag=="undefined"&&(h.busy.flag=!1);if(!h.busy.flag){u(h.busy.timeout);var t=function(){var e,n,r;if(h.busy.flag)return;for(e=h.queues.length-1;e>=0;--e){n=h.queues[e];if(n.length===0)continue;r=n.shift(),h.fireQueueItem(r),h.busy.timeout=o(t,h.options.busyDelay)}};h.busy.timeout=o(t,h.options.busyDelay)}return h.busy.flag},h.busy.flag=!1,h.fireQueueItem=function(e){return e.callback.apply(e.scope||h,e.args||[])},h.pushQueue=function(e){return h.queues[e.queue||0]=h.queues[e.queue||0]||[],h.queues[e.queue||0].push(e),h},h.queue=function(e,t){return typeof e=="function"&&(e={callback:e}),typeof t!="undefined"&&(e.queue=t),h.busy()?h.pushQueue(e):h.fireQueueItem(e),h},h.clearQueue=function(){return h.busy.flag=!1,h.queues=[],h},h.stateChanged=!1,h.doubleChecker=!1,h.doubleCheckComplete=function(){return h.stateChanged=!0,h.doubleCheckClear(),h},h.doubleCheckClear=function(){return h.doubleChecker&&(u(h.doubleChecker),h.doubleChecker=!1),h},h.doubleCheck=function(e){return h.stateChanged=!1,h.doubleCheckClear(),h.bugs.ieDoubleCheck&&(h.doubleChecker=o(function(){return h.doubleCheckClear(),h.stateChanged||e(),!0},h.options.doubleCheckInterval)),h},h.safariStatePoll=function(){var t=h.extractState(h.getLocationHref()),n;if(!h.isLastSavedState(t))return n=t,n||(n=h.createStateObject()),h.Adapter.trigger(e,"popstate"),h;return},h.back=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.back,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.back(!1)}),p.go(-1),!0)},h.forward=function(e){return e!==!1&&h.busy()?(h.pushQueue({scope:h,callback:h.forward,args:arguments,queue:e}),!1):(h.busy(!0),h.doubleCheck(function(){h.forward(!1)}),p.go(1),!0)},h.go=function(e,t){var n;if(e>0)for(n=1;n<=e;++n)h.forward(t);else{if(!(e<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(n=-1;n>=e;--n)h.back(t)}return h};if(h.emulated.pushState){var v=function(){};h.pushState=h.pushState||v,h.replaceState=h.replaceState||v}else h.onPopState=function(t,n){var r=!1,i=!1,s,o;return h.doubleCheckComplete(),s=h.getHash(),s?(o=h.extractState(s||h.getLocationHref(),!0),o?h.replaceState(o.data,o.title,o.url,!1):(h.Adapter.trigger(e,"anchorchange"),h.busy(!1)),h.expectedStateId=!1,!1):(r=h.Adapter.extractEventData("state",t,n)||!1,r?i=h.getStateById(r):h.expectedStateId?i=h.getStateById(h.expectedStateId):i=h.extractState(h.getLocationHref()),i||(i=h.createStateObject(null,null,h.getLocationHref())),h.expectedStateId=!1,h.isLastSavedState(i)?(h.busy(!1),!1):(h.storeState(i),h.saveState(i),h.setTitle(i),h.Adapter.trigger(e,"statechange"),h.busy(!1),!0))},h.Adapter.bind(e,"popstate",h.onPopState),h.pushState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.pushState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.pushState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0},h.replaceState=function(t,n,r,i){if(h.getHashByUrl(r)&&h.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(i!==!1&&h.busy())return h.pushQueue({scope:h,callback:h.replaceState,args:arguments,queue:i}),!1;h.busy(!0);var s=h.createStateObject(t,n,r);return h.isLastSavedState(s)?h.busy(!1):(h.storeState(s),h.expectedStateId=s.id,p.replaceState(s.id,s.title,s.url),h.Adapter.trigger(e,"popstate")),!0};if(s){try{h.store=l.parse(s.getItem("History.store"))||{}}catch(m){h.store={}}h.normalizeStore()}else h.store={},h.normalizeStore();h.Adapter.bind(e,"unload",h.clearAllIntervals),h.saveState(h.storeState(h.extractState(h.getLocationHref(),!0))),s&&(h.onUnload=function(){var e,t,n;try{e=l.parse(s.getItem("History.store"))||{}}catch(r){e={}}e.idToState=e.idToState||{},e.urlToId=e.urlToId||{},e.stateToId=e.stateToId||{};for(t in h.idToState){if(!h.idToState.hasOwnProperty(t))continue;e.idToState[t]=h.idToState[t]}for(t in h.urlToId){if(!h.urlToId.hasOwnProperty(t))continue;e.urlToId[t]=h.urlToId[t]}for(t in h.stateToId){if(!h.stateToId.hasOwnProperty(t))continue;e.stateToId[t]=h.stateToId[t]}h.store=e,h.normalizeStore(),n=l.stringify(e);try{s.setItem("History.store",n)}catch(i){if(i.code!==DOMException.QUOTA_EXCEEDED_ERR)throw i;s.length&&(s.removeItem("History.store"),s.setItem("History.store",n))}},h.intervalList.push(a(h.onUnload,h.options.storeInterval)),h.Adapter.bind(e,"beforeunload",h.onUnload),h.Adapter.bind(e,"unload",h.onUnload));if(!h.emulated.pushState){h.bugs.safariPoll&&h.intervalList.push(a(h.safariStatePoll,h.options.safariPollInterval));if(i.vendor==="Apple Computer, Inc."||(i.appCodeName||"")==="Mozilla")h.Adapter.bind(e,"hashchange",function(){h.Adapter.trigger(e,"popstate")}),h.getHash()&&h.Adapter.onDomLoad(function(){h.Adapter.trigger(e,"hashchange")})}},(!h.options||!h.options.delayInit)&&h.init()}(window)]]>
+
-
- a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML=" ",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML=" ","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML=" ",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0;
}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML=" a ",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""," "],legend:[1,""," "],area:[1,""," "],param:[1,""," "],thead:[1,""],tr:[2,""],col:[2,""],td:[3,""],_default:l.htmlSerialize?[0,"",""]:[1,"X","
"]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h ]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/