diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..ef6e90b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# editorconfig tool configuration +# see http://editorconfig.org for docs + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +trim_trailing_whitespace = true +insert_final_newline = true +quote_type = single +max_line_length = off + +[*.js] +quote_type = double +ij_javascript_use_double_quotes = true + +[*.yml] +indent_style = space +indent_size = 2 + +[*.xml] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[composer.json] +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..4be1c918 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Disable LF normalization for all files +* -text \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0d32de00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +favicon.* +.htconfig.php +\#* +*.log +*.out +*.version* +favicon.* +*~ + +#ignore reports, should be generted with every build +report/ + +#ignore config files from eclipse, we don't want IDE files in our repository +.project +.buildpath +.externalToolBuilders +.settings +#ignore OSX .DS_Store files +.DS_Store + +/nbproject/ + +#ignore smarty cache +/view/smarty3/compiled/ diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..8542c680 --- /dev/null +++ b/.tx/config @@ -0,0 +1,399 @@ +[main] +host = https://api.transifex.com + +[o:Friendica:p:friendica:r:addon_advancedcontentfilter_messagespo] +file_filter = advancedcontentfilter/lang//messages.po +source_file = advancedcontentfilter/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_blackout_messagespo] +file_filter = blackout/lang//messages.po +source_file = blackout/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_blockem_messagespo] +file_filter = blockem/lang//messages.po +source_file = blockem/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_blogger_messagespo] +file_filter = blogger/lang//messages.po +source_file = blogger/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_buffer_messagespo] +file_filter = buffer/lang//messages.po +source_file = buffer/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_buglink_messagespo] +file_filter = buglink/lang//messages.po +source_file = buglink/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_catavatar_messagespo] +file_filter = catavatar/lang//messages.po +source_file = catavatar/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_cookienotice_messagespo] +file_filter = cookienotice/lang//messages.po +source_file = cookienotice/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_curweather_messagespo] +file_filter = curweather/lang//messages.po +source_file = curweather/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_diaspora_messagespo] +file_filter = diaspora/lang//messages.po +source_file = diaspora/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_dwpost_messagespo] +file_filter = dwpost/lang//messages.po +source_file = dwpost/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_forumdirectory_messagespo] +file_filter = forumdirectory/lang//messages.po +source_file = forumdirectory/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_fromapp_messagespo] +file_filter = fromapp/lang//messages.po +source_file = fromapp/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_fromgplus_messagespo] +file_filter = fromgplus/lang//messages.po +source_file = fromgplus/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_geonames_messagespo] +file_filter = geonames/lang//messages.po +source_file = geonames/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_gnot_messagespo] +file_filter = gnot/lang//messages.po +source_file = gnot/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_gravatar_messagespo] +file_filter = gravatar/lang//messages.po +source_file = gravatar/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_group_text_messagespo] +file_filter = group_text/lang//messages.po +source_file = group_text/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_ifttt_messagespo] +file_filter = ifttt/lang//messages.po +source_file = ifttt/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_ijpost_messagespo] +file_filter = ijpost/lang//messages.po +source_file = ijpost/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_impressum_messagespo] +file_filter = impressum/lang//messages.po +source_file = impressum/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_infiniteimprobabilitydrive_messagespo] +file_filter = infiniteimprobabilitydrive/lang//messages.po +source_file = infiniteimprobabilitydrive/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_irc_messagespo] +file_filter = irc/lang//messages.po +source_file = irc/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_jappixmini_messagespo] +file_filter = jappixmini/lang//messages.po +source_file = jappixmini/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_js_upload_messagespo] +file_filter = js_upload/lang//messages.po +source_file = js_upload/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_krynn_messagespo] +file_filter = krynn/lang//messages.po +source_file = krynn/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_langfilter_messagespo] +file_filter = langfilter/lang//messages.po +source_file = langfilter/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_libertree_messagespo] +file_filter = libertree/lang//messages.po +source_file = libertree/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_libravatar_messagespo] +file_filter = libravatar/lang//messages.po +source_file = libravatar/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_ljpost_messagespo] +file_filter = ljpost/lang//messages.po +source_file = ljpost/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_mailstream_messagespo] +file_filter = mailstream/lang//messages.po +source_file = mailstream/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_mathjax_messagespo] +file_filter = mathjax/lang//messages.po +source_file = mathjax/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_membersince_messagespo] +file_filter = membersince/lang//messages.po +source_file = membersince/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_morechoice_messagespo] +file_filter = morechoice/lang//messages.po +source_file = morechoice/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_morepokes_messagespo] +file_filter = morepokes/lang//messages.po +source_file = morepokes/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_newmemberwidget_messagespo] +file_filter = newmemberwidget/lang//messages.po +source_file = newmemberwidget/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_notifyall_messagespo] +file_filter = notifyall/lang//messages.po +source_file = notifyall/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_notimeline_messagespo] +file_filter = notimeline/lang//messages.po +source_file = notimeline/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_nsfw_messagespo] +file_filter = nsfw/lang//messages.po +source_file = nsfw/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_numfriends_messagespo] +file_filter = numfriends/lang//messages.po +source_file = numfriends/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_openstreetmap_messagespo] +file_filter = openstreetmap/lang//messages.po +source_file = openstreetmap/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_pageheader_messagespo] +file_filter = pageheader/lang//messages.po +source_file = pageheader/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_piwik_messagespo] +file_filter = piwik/lang//messages.po +source_file = piwik/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_planets_messagespo] +file_filter = planets/lang//messages.po +source_file = planets/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_public_server_messagespo] +file_filter = public_server/lang//messages.po +source_file = public_server/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_pumpio_messagespo] +file_filter = pumpio/lang//messages.po +source_file = pumpio/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_qcomment_messagespo] +file_filter = qcomment/lang//messages.po +source_file = qcomment/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_randplace_messagespo] +file_filter = randplace/lang//messages.po +source_file = randplace/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_remote_permissions_messagespo] +file_filter = remote_permissions/lang//messages.po +source_file = remote_permissions/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_rendertime_messagespo] +file_filter = rendertime/lang//messages.po +source_file = rendertime/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_securemail_messagespo] +file_filter = securemail/lang//messages.po +source_file = securemail/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_showmore_messagespo] +file_filter = showmore/lang//messages.po +source_file = showmore/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_smileybutton_messagespo] +file_filter = smileybutton/lang//messages.po +source_file = smileybutton/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_startpage_messagespo] +file_filter = startpage/lang//messages.po +source_file = startpage/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_statusnet_messagespo] +file_filter = statusnet/lang//messages.po +source_file = statusnet/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_superblock_messagespo] +file_filter = superblock/lang//messages.po +source_file = superblock/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_testdrive_messagespo] +file_filter = testdrive/lang//messages.po +source_file = testdrive/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_tictac_messagespo] +file_filter = tictac/lang//messages.po +source_file = tictac/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_tumblr_messagespo] +file_filter = tumblr/lang//messages.po +source_file = tumblr/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_twitter_messagespo] +file_filter = twitter/lang//messages.po +source_file = twitter/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_viewsrc_messagespo] +file_filter = viewsrc/lang//messages.po +source_file = viewsrc/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_webrtc_messagespo] +file_filter = webrtc/lang//messages.po +source_file = webrtc/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_widgets_messagespo] +file_filter = widgets/lang//messages.po +source_file = widgets/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_windowsphonepush_messagespo] +file_filter = windowsphonepush/lang//messages.po +source_file = windowsphonepush/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_wppost_messagespo] +file_filter = wppost/lang//messages.po +source_file = wppost/lang/C/messages.po +source_lang = en +type = PO + +[o:Friendica:p:friendica:r:addon_xmpp_messagespo] +file_filter = xmpp/lang//messages.po +source_file = xmpp/lang/C/messages.po +source_lang = en +type = PO + diff --git a/.woodpecker/.code_standards_check.yml b/.woodpecker/.code_standards_check.yml new file mode 100644 index 00000000..60a1bd2a --- /dev/null +++ b/.woodpecker/.code_standards_check.yml @@ -0,0 +1,76 @@ +skip_clone: true + +steps: + clone_friendica_base: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone https://github.com/friendica/friendica.git . + - git checkout $CI_COMMIT_BRANCH + when: + event: pull_request + clone_friendica_addon: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git clone $CI_REPO_CLONE_URL addon + - cd addon/ + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA + when: + event: pull_request + restore_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + restore: true + cache_key: '{{ .Repo.Name }}_phpcs_{{ arch }}_{{ os }}' + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + when: + event: pull_request + composer_install: + image: composer + commands: + - export COMPOSER_HOME=.composer + - ./bin/composer.phar run cs:install + when: + event: pull_request + rebuild_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + rebuild: true + cache_key: '{{ .Repo.Name }}_phpcs_{{ arch }}_{{ os }}' + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + when: + event: pull_request + phpstan: + image: friendicaci/php8.3:php8.3.3 + commands: + - ./bin/composer.phar run phpstan; + check: + image: friendicaci/php-cs + commands: + - cd addon/ + - if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then + git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH}; + export CHANGED_FILES="$(git diff --name-status $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA} | grep ^A | cut -f2 | sed -e "s/^/addon\\//")"; + else + export CHANGED_FILES="$(git diff --name-status ${CI_COMMIT_SHA} | grep ^A | cut -f2 | sed -e "s/^/addon\\//")"; + fi + - cd ../ + - /check-php-cs.sh + when: + event: pull_request diff --git a/.woodpecker/.continuous-deployment.yml b/.woodpecker/.continuous-deployment.yml new file mode 100644 index 00000000..6b41deae --- /dev/null +++ b/.woodpecker/.continuous-deployment.yml @@ -0,0 +1,115 @@ +# This prevents executing this pipeline at other servers than ci.friendi.ca +labels: + location: friendica + type: releaser + +skip_clone: true + +steps: + clone_friendica_base: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone https://github.com/friendica/friendica.git . + - git checkout $CI_COMMIT_BRANCH + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + clone_friendica_addon: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git clone $CI_REPO_CLONE_URL addon + - cd addon/ + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + restore_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + restore: true + cache_key: "{{ .Repo.Name }}_php7.4_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + composer_install: + image: friendicaci/php8.2:php8.2.16 + commands: + - export COMPOSER_HOME=.composer + - composer validate + - composer install --no-dev --optimize-autoloader + volumes: + - /etc/hosts:/etc/hosts + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + create_artifacts: + image: debian + commands: + - apt-get update + - apt-get install bzip2 + - mkdir ./build + - export VERSION="$(cat VERSION)" + - export RELEASE="friendica-addons-$VERSION" + - export ARTIFACT="$RELEASE.tar.gz" + - tar + --exclude='.tx' + --exclude='.git' + --exclude='.editorconfig' + --exclude='.gitattributes' + --exclude='.gitignore' + --exclude='.woodpecker' + --exclude='**/*/messages.po' + -cvzf ./build/$ARTIFACT addon/ + - cd ./build + - sha256sum "$ARTIFACT" > "$ARTIFACT.sum256" + - chmod 664 ./* + - ls -lh + - cat "$ARTIFACT.sum256" + - sha256sum "$ARTIFACT" + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + sign_artifacts: + image: plugins/gpgsign + settings: + key: + from_secret: gpg_key + passphrase: + from_secret: gpg_password + files: + - build/* + exclude: + - build/*.sum256 + detach_sign: true + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push + publish_artifacts: + image: alpine + commands: + - cp -fr build/* /tmp/friendica_files/ + volumes: + - files:/tmp/friendica_files + when: + repo: friendica/friendica-addons + branch: [ develop, '*-rc' ] + event: push diff --git a/.woodpecker/.messages.po_check.yml b/.woodpecker/.messages.po_check.yml new file mode 100644 index 00000000..e0239dcd --- /dev/null +++ b/.woodpecker/.messages.po_check.yml @@ -0,0 +1,40 @@ +skip_clone: true + +steps: + clone_friendica_base: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone https://github.com/friendica/friendica.git . + - git checkout $CI_COMMIT_BRANCH + when: + event: pull_request + clone_friendica_addon: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git clone $CI_REPO_CLONE_URL addon + - cd addon/ + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA + when: + event: pull_request + build_xgettext: + image: friendicaci/transifex + commands: + - /xgettext-addon.sh + when: + event: pull_request + check: + image: friendicaci/transifex + commands: + - /check-addons.sh + when: + event: pull_request + +branches: + exclude: [ stable ] diff --git a/.woodpecker/.phpunit.yml b/.woodpecker/.phpunit.yml new file mode 100644 index 00000000..8b840f80 --- /dev/null +++ b/.woodpecker/.phpunit.yml @@ -0,0 +1,118 @@ +matrix: + include: + - PHP_MAJOR_VERSION: 7.4 + PHP_VERSION: 7.4.33 + - PHP_MAJOR_VERSION: 8.0 + PHP_VERSION: 8.0.30 + - PHP_MAJOR_VERSION: 8.1 + PHP_VERSION: 8.1.27 + - PHP_MAJOR_VERSION: 8.2 + PHP_VERSION: 8.2.16 + - PHP_MAJOR_VERSION: 8.3 + PHP_VERSION: 8.3.3 + +# This forces PHP Unit executions at the "opensocial" labeled location (because of much more power...) +labels: + location: opensocial + +skip_clone: true + +steps: + clone_friendica_base: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone https://github.com/friendica/friendica.git . + - git checkout $CI_COMMIT_BRANCH + clone_friendica_addon: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git clone $CI_REPO_CLONE_URL addon + - cd addon/ + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA + restore_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + restore: true + cache_key: "{{ .Repo.Name }}_php${PHP_MAJOR_VERSION}_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + composer_install: + image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} + commands: + - export COMPOSER_HOME=.composer + - ./bin/composer.phar validate + - ./bin/composer.phar install --prefer-dist + volumes: + - /etc/hosts:/etc/hosts + rebuild_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + rebuild: true + cache_key: "{{ .Repo.Name }}_php${PHP_MAJOR_VERSION}_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + test: + image: friendicaci/php${PHP_MAJOR_VERSION}:php${PHP_VERSION} + environment: + MYSQL_HOST: "mariadb" + MYSQL_PORT: "3306" + MYSQL_DATABASE: "test" + MYSQL_PASSWORD: "test" + MYSQL_USER: "test" + REDIS_HOST: "redis" + MEMCACHED_HOST: "memcached" + MEMCACHE_HOST: "memcached" + commands: + - cp config/local-sample.config.php config/local.config.php + - if ! bin/wait-for-connection $MYSQL_HOST $MYSQL_PORT 300; then echo "[ERROR] Waited 300 seconds, no response" >&2; exit 1; fi + - mysql -h$MYSQL_HOST -P$MYSQL_PORT -p$MYSQL_PASSWORD -u$MYSQL_USER $MYSQL_DATABASE < database.sql + - if [ "${PHP_MAJOR_VERSION}" = "8.2" -a "${CI_REPO}" = "friendica/friendica-addons" ]; then + phpenmod xdebug; + export XDEBUG_MODE=coverage; + phpunit --configuration tests/phpunit-addons.xml --coverage-clover clover.xml; + else + phpunit --configuration tests/phpunit-addons.xml; + fi + codecov: + image: friendicaci/codecov + when: + matrix: + PHP_MAJOR_VERSION: 8.2 + PHP_VERSION: 8.2.16 + repo: + - friendica/friendica-addons + commands: + - codecov -R '.' -Z -f 'clover.xml' + environment: + CODECOV_TOKEN: + from_secret: codecov-token + +services: + mariadb: + image: mariadb:latest + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "true" + MYSQL_DATABASE: "test" + MYSQL_PASSWORD: "test" + MYSQL_USER: "test" + + memcached: + image: memcached + + redis: + image: redis diff --git a/.woodpecker/.releaser.yml b/.woodpecker/.releaser.yml new file mode 100644 index 00000000..2d880a6b --- /dev/null +++ b/.woodpecker/.releaser.yml @@ -0,0 +1,108 @@ +# This prevents executing this pipeline at other servers than ci.friendi.ca +labels: + location: friendica + type: releaser + +skip_clone: true + +steps: + clone_friendica_base: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git config --global --add safe.directory $CI_WORKSPACE + - git clone https://github.com/friendica/friendica.git . + - git checkout $CI_COMMIT_BRANCH + when: + repo: friendica/friendica-addons + event: tag + clone_friendica_addon: + image: alpine/git + commands: + - git config --global user.email "no-reply@friendi.ca" + - git config --global user.name "Friendica" + - git clone $CI_REPO_CLONE_URL addon + - cd addon/ + - git checkout $CI_COMMIT_BRANCH + - git fetch origin $CI_COMMIT_REF + - git merge $CI_COMMIT_SHA + when: + repo: friendica/friendica-addons + event: tag + restore_cache: + image: meltwater/drone-cache:dev + settings: + backend: "filesystem" + restore: true + cache_key: "{{ .Repo.Name }}_php7.4_{{ arch }}_{{ os }}" + archive_format: "gzip" + mount: + - '.composer' + volumes: + - /tmp/drone-cache:/tmp/cache + when: + repo: friendica/friendica-addons + event: tag + composer_install: + image: friendicaci/php8.2:php8.2.16 + commands: + - export COMPOSER_HOME=.composer + - composer validate + - composer install --no-dev --optimize-autoloader + when: + repo: friendica/friendica-addons + event: tag + volumes: + - /etc/hosts:/etc/hosts + create_artifacts: + image: debian + commands: + - apt-get update + - apt-get install bzip2 + - mkdir ./build + - export VERSION="$(cat VERSION)" + - export RELEASE="friendica-addons-$VERSION" + - export ARTIFACT="$RELEASE.tar.gz" + - tar + --exclude='.tx' + --exclude='.git' + --exclude='.editorconfig' + --exclude='.gitattributes' + --exclude='.gitignore' + --exclude='.woodpecker' + --exclude='**/*/messages.po' + -cvzf ./build/$ARTIFACT addon/ + - cd ./build + - sha256sum "$ARTIFACT" > "$ARTIFACT.sum256" + - chmod 664 ./* + - ls -lh + - cat "$ARTIFACT.sum256" + - sha256sum "$ARTIFACT" + when: + repo: friendica/friendica-addons + event: tag + sign_artifacts: + image: plugins/gpgsign + settings: + key: + from_secret: gpg_key + passphrase: + from_secret: gpg_password + files: + - build/* + exclude: + - build/*.sum256 + detach_sign: true + when: + repo: friendica/friendica-addons + event: tag + publish_artifacts: + image: alpine + commands: + - cp -fr build/* /tmp/friendica_files/ + volumes: + - files:/tmp/friendica_files + when: + repo: friendica/friendica-addons + event: tag diff --git a/INSTALL.txt b/INSTALL.txt new file mode 100644 index 00000000..37998583 --- /dev/null +++ b/INSTALL.txt @@ -0,0 +1,18 @@ +********************* +* Install Using Git * +********************* + +To install all addons using git, cd into your top level Friendica directory and + + git clone https://github.com/friendica/friendica-addons.git addon + +This will clone the entire repository in a directory called addon. They can now be activated in the addons section of your admin panel. + +******************** +* Install Manually * +******************** + +1. Download the archive (Download ZIP button) containing the addons. +2. Unzip the contents of the archive to your harddrive. +3. Upload the extracted directory and all it's contents to /path/to/friendica/addon. You will need to create the addon directory if this is the first addon you have installed. +4. Activate the addon in the addons section of your admin panel. diff --git a/Makefile b/Makefile deleted file mode 100644 index 8dd9f543..00000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ - -all: - @./buildtgz - diff --git a/README.md b/README.md new file mode 100644 index 00000000..6a3202bb --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +Addons for Friendica +==================== + +This repository is a collection of addons for the [Friendica Social Communications Server](https://github.com/friendica/friendica). +You can add these addons to the /addon directory of your Friendica installation do extend the functionality of your node. + +After uploading the addons to your server, you need to activate the desired addons in the Admin panel. Addons not activated have no effect on your node. + +## Issues + +Please report any issues you have with addons of Friendica from this repository in [the main issue tracker of the project](https://github.com/friendica/friendica/issues). + +## Connectors + +Among these addons there are also the [connectors](https://github.com/friendica/friendica/blob/stable/doc/Connectors.md) for various other networks (e.g. Twitter, pump.io, Google+) that are needed for communication when the protocol is not supported by Friendica core (DFRN, OStatus and Diaspora). + +For communication with contacts in networks supporting those (e.g. GNU social, Diaspora and red#matrix) you just need to access the page configuration in the Admin panel and enable them. For networks where communication is only possible the API access to a remote account, you need to activate the fitting connectors. + +## Development + +The addon interface of Friendica is very flexible and powerful, so if you are missing functionality, your chances are high it may be added with an addon. +See the [documentation](https://github.com/friendica/friendica/blob/stable/doc/Addons.md) for more information on the addon development. + +## Translation + +Addons can be translated like any other part of Friendica. +Translation for addons is done at [the Transifex Friendica page](https://www.transifex.com/Friendica/friendica/dashboard/). + +Read more about the workflow in the [Friendica translation documentation](https://github.com/friendica/friendica/blob/stable/doc/translations.md#addon). diff --git a/advancedcontentfilter/LICENSE b/advancedcontentfilter/LICENSE new file mode 100644 index 00000000..e877cc1c --- /dev/null +++ b/advancedcontentfilter/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2011-2018 Hypolite Petovan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + * Neither the name of Friendica nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL FRIENDICA BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/advancedcontentfilter/README.md b/advancedcontentfilter/README.md new file mode 100644 index 00000000..de16d6f2 --- /dev/null +++ b/advancedcontentfilter/README.md @@ -0,0 +1,11 @@ +Advanced Content Filter +======================= + +Main author Hypolite Petovan. + + +## License + +The _Advanced Content Filter_ addon is licensed under the [3-clause BSD license][2] see the LICENSE file in the addons directory. + +[1]: http://opensource.org/licenses/BSD-3-Clause diff --git a/advancedcontentfilter/advancedcontentfilter.js b/advancedcontentfilter/advancedcontentfilter.js new file mode 100644 index 00000000..a8fc1c65 --- /dev/null +++ b/advancedcontentfilter/advancedcontentfilter.js @@ -0,0 +1,132 @@ +$.ajaxSetup({headers: {'X-CSRF-Token': csrfToken}}); + +$.extend({ + ajaxJSON: function(method, url, data) { + return $.ajax({ + type: method.toUpperCase(), + url: url, + data: JSON.stringify(data), + contentType: 'application/json; charset=utf-8', + dataType: 'json' + }); + } +}); + +new Vue({ + el: '#rules', + + data: { + showModal: false, + errorMessage: '', + editedIndex: null, + rule: {id: '', name: '', expression: '', created: ''}, + rules: existingRules || [], + itemUrl: '', + itemJson: '', + currentTheme: currentTheme, + messages: messages, + }, + + watch: { + showModal: function () { + if (this.showModal) { + $(this.$refs.vuemodal).modal('show'); + } else { + $(this.$refs.vuemodal).modal('hide'); + } + } + }, + + methods: { + resetForm: function() { + this.rule = {id: '', name: '', expression: '', created: ''}; + this.showModal = false; + this.editedIndex = null; + }, + + addRule: function () { + if (this.rule.name.trim()) { + this.errorMessage = ''; + + var self = this; + $.ajaxJSON('post', '/advancedcontentfilter/api/rules', this.rule) + .then(function (responseJSON) { + self.rules.push(responseJSON.rule); + self.resetForm(); + }, function (response) { + self.errorMessage = response.responseJSON.exception[0].message; + }); + } + }, + + editRule: function (rule) { + this.editedIndex = this.rules.indexOf(rule); + this.rule = Object.assign({}, rule); + this.showModal = true; + }, + + saveRule: function (rule) { + this.errorMessage = ''; + + var self = this; + $.ajaxJSON('put', '/advancedcontentfilter/api/rules/' + rule.id, rule) + .then(function () { + self.rules[self.editedIndex] = rule; + self.resetForm(); + }, function (response) { + self.errorMessage = response.responseJSON.exception[0].message; + }); + }, + + toggleActive: function (rule) { + var previousValue = this.rules[this.rules.indexOf(rule)].active; + var newValue = Math.abs(parseInt(rule.active) - 1); + + this.rules[this.rules.indexOf(rule)].active = newValue; + + var self = this; + $.ajaxJSON('put', '/advancedcontentfilter/api/rules/' + rule.id, {'active': newValue}) + .fail(function (response) { + self.rules[self.rules.indexOf(rule)].active = previousValue; + console.log(response.responseJSON.message); + }); + }, + + deleteRule: function (rule) { + if (confirm('Are you sure you want to delete this rule?')) { + var self = this; + $.ajaxJSON('delete', '/advancedcontentfilter/api/rules/' + rule.id) + .then(function () { + self.rules.splice(self.rules.indexOf(rule), 1); + }, function (response) { + console.log(response.responseJSON.message); + }); + } + }, + + showVariables: function () { + var urlParts = this.itemUrl.split('/'); + var guid = urlParts[urlParts.length - 1]; + + this.itemJson = ''; + + var self = this; + $.ajaxJSON('get', '/advancedcontentfilter/api/variables/' + guid) + .then(function (responseJSON) { + self.itemJson = responseJSON.variables; + }, function (response) { + self.itemJson = response.responseJSON.message; + }); + + return false; + } + }, + + // These render functions have been compiled from templates/advancedcontentfilter.vue, check this file out for instructions + render: function () { + with(this){return _c('div',{attrs:{"id":"rules"}},[_c('p',[_c('a',{attrs:{"href":"settings/addons"}},[_v("🔙 "+_s(messages.backtosettings))])]),_c('h1',[_v(_s(messages.title)+" "),_c('a',{staticClass:"btn btn-default btn-sm",attrs:{"href":"advancedcontentfilter/help","title":messages.help}},[_c('i',{staticClass:"fa fa-question fa-2x",attrs:{"aria-hidden":"true"}})])]),_c('div',[_v(_s(messages.intro))]),_c('h2',[_v(_s(messages.your_rules)+" "),_c('button',{staticClass:"btn btn-primary btn-sm",attrs:{"title":messages.add_a_rule},on:{"click":function($event){showModal = true}}},[_c('i',{staticClass:"fa fa-plus fa-2x",attrs:{"aria-hidden":"true"}})])]),(rules.length === 0)?_c('div',{},[_v(_s(messages.no_rules))]):_e(),_c('ul',{staticClass:"list-group"},_l((rules),function(rule){return _c('li',{staticClass:"list-group-item"},[_c('p',{staticClass:"pull-right"},[(parseInt(rule.active))?_c('button',{staticClass:"btn btn-xs btn-primary",attrs:{"type":"button","aria-label":messages.disable_this_rule,"title":messages.disable_this_rule},on:{"click":function($event){toggleActive(rule)}}},[_c('i',{staticClass:"fa fa-toggle-on",attrs:{"aria-hidden":"true"}}),_v(" "+_s(messages.enabled))]):_c('button',{staticClass:"btn btn-xs btn-default",attrs:{"type":"button","aria-label":messages.enable_this_rule,"title":messages.enable_this_rule},on:{"click":function($event){toggleActive(rule)}}},[_c('i',{staticClass:"fa fa-toggle-off",attrs:{"aria-hidden":"true"}}),_v(" "+_s(messages.disabled))]),_v(" "),_c('button',{staticClass:"btn btn-xs btn-primary",attrs:{"type":"button","aria-label":messages.edit_this_rule,"title":messages.edit_this_rule},on:{"click":function($event){editRule(rule)}}},[_c('i',{staticClass:"fa fa-pencil",attrs:{"aria-hidden":"true"}})]),_v(" "),_c('button',{staticClass:"btn btn-xs btn-default",attrs:{"type":"button","aria-label":messages.delete_this_rule,"title":messages.delete_this_rule},on:{"click":function($event){deleteRule(rule)}}},[_c('i',{staticClass:"fa fa-trash-o",attrs:{"aria-hidden":"true"}})])]),_c('h3',{staticClass:"list-group-item-heading"},[_v(_s(messages.rule)+" #"+_s(rule.id)+": "+_s(rule.name))]),(rule.expression)?_c('pre',{staticClass:"list-group-item-text"},[_v(_s(rule.expression))]):_e()])})),_c('div',{ref:"vuemodal",staticClass:"modal fade",attrs:{"tabindex":"-1","role":"dialog"}},[_c('div',{staticClass:"modal-dialog",attrs:{"role":"document"}},[_c('div',{staticClass:"modal-content"},[_c('div',{staticClass:"modal-header"},[(currentTheme === 'frio')?_c('button',{staticClass:"close",attrs:{"type":"button","data-dismiss":"modal","aria-label":messages.close},on:{"click":function($event){showModal = false}}},[_c('span',{attrs:{"aria-hidden":"true"}},[_v("×")])]):_e(),(rule.id)?_c('h3',[_v(_s(messages.edit_the_rule)+" \""+_s(rule.name)+"\"")]):_e(),(!rule.id)?_c('h3',[_v(_s(messages.add_a_rule))]):_e()]),_c('div',{staticClass:"modal-body"},[_c('form',[(errorMessage)?_c('div',{staticClass:"alert alert-danger",attrs:{"role":"alert"}},[_v(_s(errorMessage))]):_e(),_c('div',{staticClass:"form-group"},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(rule.name),expression:"rule.name"}],staticClass:"form-control",attrs:{"placeholder":messages.rule_name},domProps:{"value":(rule.name)},on:{"input":function($event){if($event.target.composing)return;$set(rule, "name", $event.target.value)}}})]),_c('div',{staticClass:"form-group"},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(rule.expression),expression:"rule.expression"}],staticClass:"form-control",attrs:{"placeholder":messages.rule_expression},domProps:{"value":(rule.expression)},on:{"input":function($event){if($event.target.composing)return;$set(rule, "expression", $event.target.value)}}})])])]),_c('div',{staticClass:"modal-footer"},[_c('button',{staticClass:"btn btn-default",attrs:{"type":"button","data-dismiss":"modal","aria-label":"Close"},on:{"click":function($event){resetForm()}}},[_v(_s(messages.cancel))]),(rule.id)?_c('button',{staticClass:"btn btn-primary",attrs:{"slot":"button","type":"button"},on:{"click":function($event){saveRule(rule)}},slot:"button"},[_v(_s(messages.save_this_rule))]):_e(),(!rule.id)?_c('button',{staticClass:"btn btn-primary",attrs:{"slot":"button","type":"button"},on:{"click":function($event){addRule()}},slot:"button"},[_v(_s(messages.add_a_rule))]):_e()])])])]),_c('form',{staticClass:"form-inline",on:{"submit":function($event){$event.preventDefault();showVariables()}}},[_c('fieldset',[_c('legend',[_v("Show post variables")]),_c('div',{staticClass:"form-group",staticStyle:{"width":"50%"}},[_c('label',{staticClass:"sr-only",attrs:{"for":"itemUrl"}},[_v("Post URL or item guid")]),_c('input',{directives:[{name:"model",rawName:"v-model",value:(itemUrl),expression:"itemUrl"}],staticClass:"form-control",staticStyle:{"width":"100%"},attrs:{"id":"itemUrl","placeholder":"Post URL or item guid"},domProps:{"value":(itemUrl)},on:{"input":function($event){if($event.target.composing)return;itemUrl=$event.target.value}}})]),_c('button',{staticClass:"btn btn-primary",attrs:{"type":"submit"}},[_v("Show Variables")])])]),_c('pre',{},[_v(_s(itemJson))])])} + }, + + staticRenderFns: [ + ], +}); diff --git a/advancedcontentfilter/advancedcontentfilter.php b/advancedcontentfilter/advancedcontentfilter.php new file mode 100644 index 00000000..aeeff77c --- /dev/null +++ b/advancedcontentfilter/advancedcontentfilter.php @@ -0,0 +1,485 @@ + + * Maintainer: Hypolite Petovan + * + * Copyright (c) 2018 Hypolite Petovan + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * * copyright notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * * Neither the name of Friendica nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL FRIENDICA BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +use Friendica\BaseModule; +use Friendica\Content\Text\Markdown; +use Friendica\Core\Hook; +use Friendica\Core\Renderer; +use Friendica\Database\DBA; +use Friendica\Database\DBStructure; +use Friendica\DI; +use Friendica\Model\Post; +use Friendica\Model\Tag; +use Friendica\Model\User; +use Friendica\Module\Security\Login; +use Friendica\Network\HTTPException; +use Friendica\Util\DateTimeFormat; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Symfony\Component\ExpressionLanguage; + +require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; + +function advancedcontentfilter_install() +{ + Hook::register('dbstructure_definition' , __FILE__, 'advancedcontentfilter_dbstructure_definition'); + Hook::register('prepare_body_content_filter', __FILE__, 'advancedcontentfilter_prepare_body_content_filter'); + Hook::register('addon_settings' , __FILE__, 'advancedcontentfilter_addon_settings'); + + Hook::add('dbstructure_definition' , __FILE__, 'advancedcontentfilter_dbstructure_definition'); + DBStructure::performUpdate(); + + DI::logger()->notice('installed advancedcontentfilter'); +} + +/* + * Hooks + */ + +function advancedcontentfilter_dbstructure_definition(&$database) +{ + $database['advancedcontentfilter_rules'] = [ + 'comment' => 'Advancedcontentfilter addon rules', + 'fields' => [ + 'id' => ['type' => 'int unsigned', 'not null' => '1', 'extra' => 'auto_increment', 'primary' => '1', 'comment' => 'Auto incremented rule id'], + 'uid' => ['type' => 'int unsigned', 'not null' => '1', 'comment' => 'Owner user id'], + 'name' => ['type' => 'varchar(255)', 'not null' => '1', 'comment' => 'Rule name'], + 'expression' => ['type' => 'mediumtext' , 'not null' => '1', 'comment' => 'Expression text'], + 'serialized' => ['type' => 'mediumtext' , 'not null' => '1', 'comment' => 'Serialized parsed expression'], + 'active' => ['type' => 'boolean' , 'not null' => '1', 'default' => '1', 'comment' => 'Whether the rule is active or not'], + 'created' => ['type' => 'datetime' , 'not null' => '1', 'default' => DBA::NULL_DATETIME, 'comment' => 'Creation date'], + ], + 'indexes' => [ + 'PRIMARY' => ['id'], + 'uid_active' => ['uid', 'active'], + ] + ]; +} + +/** + * @param array $item Prepared by either Model\Item::prepareBody or advancedcontentfilter_prepare_item_row + * @return array + */ +function advancedcontentfilter_get_filter_fields(array $item) +{ + $vars = []; + + // Convert the language JSON text into a filterable format + if (!empty($item['language']) && ($languages = json_decode($item['language'], true))) { + foreach ($languages as $key => $value) { + $vars['language_' . strtolower($key)] = $value; + } + } + + foreach ($item as $key => $value) { + $vars[str_replace('-', '_', $key)] = $value; + } + + ksort($vars); + + return $vars; +} + +function advancedcontentfilter_prepare_body_content_filter(&$hook_data) +{ + static $expressionLanguage; + + if (is_null($expressionLanguage)) { + $expressionLanguage = new ExpressionLanguage\ExpressionLanguage(); + } + + if (!DI::userSession()->getLocalUserId()) { + return; + } + + $vars = advancedcontentfilter_get_filter_fields($hook_data['item']); + + $rules = DI::cache()->get('rules_' . DI::userSession()->getLocalUserId()); + if (!isset($rules)) { + $rules = DBA::toArray(DBA::select( + 'advancedcontentfilter_rules', + ['name', 'expression', 'serialized'], + ['uid' => DI::userSession()->getLocalUserId(), 'active' => true] + )); + + DI::cache()->set('rules_' . DI::userSession()->getLocalUserId(), $rules); + } + + if ($rules) { + foreach($rules as $rule) { + try { + $serializedParsedExpression = new ExpressionLanguage\SerializedParsedExpression( + $rule['expression'], + $rule['serialized'] + ); + + // The error suppression operator is used because of potentially broken user-supplied regular expressions + $found = (bool) @$expressionLanguage->evaluate($serializedParsedExpression, $vars); + } catch (Exception $e) { + $found = false; + } + + if ($found) { + $hook_data['filter_reasons'][] = DI::l10n()->t('Filtered by rule: %s', $rule['name']); + break; + } + } + } +} + + +function advancedcontentfilter_addon_settings(array &$data) +{ + if (!DI::userSession()->getLocalUserId()) { + return; + } + + $data = [ + 'addon' => 'advancedcontentfilter', + 'title' => DI::l10n()->t('Advanced Content Filter'), + 'href' => 'advancedcontentfilter', + ]; +} + +/* + * Module + */ + +/** + * This is a statement rather than an actual function definition. The simple + * existence of this method is checked to figure out if the addon offers a + * module. + */ +function advancedcontentfilter_module() {} + +function advancedcontentfilter_init() +{ + if (DI::args()->getArgc() > 1 && DI::args()->getArgv()[1] == 'api') { + $slim = \Slim\Factory\AppFactory::create(); + + /** + * The routing middleware should be added before the ErrorMiddleware + * Otherwise exceptions thrown from it will not be handled + */ + $slim->addRoutingMiddleware(); + + $slim->addErrorMiddleware(true, true, true, DI::logger()); + + // register routes + $slim->group('/advancedcontentfilter/api', function (\Slim\Routing\RouteCollectorProxy $app) { + $app->group('/rules', function (\Slim\Routing\RouteCollectorProxy $app) { + $app->get('', 'advancedcontentfilter_get_rules'); + $app->post('', 'advancedcontentfilter_post_rules'); + + $app->get('/{id}', 'advancedcontentfilter_get_rules_id'); + $app->put('/{id}', 'advancedcontentfilter_put_rules_id'); + $app->delete('/{id}', 'advancedcontentfilter_delete_rules_id'); + }); + + $app->group('/variables', function (\Slim\Routing\RouteCollectorProxy $app) { + $app->get('/{guid}', 'advancedcontentfilter_get_variables_guid'); + }); + }); + + $slim->run(); + + exit; + } +} + +function advancedcontentfilter_content() +{ + if (!DI::userSession()->getLocalUserId()) { + return Login::form('/' . implode('/', DI::args()->getArgv())); + } + + if (DI::args()->getArgc() > 1 && DI::args()->getArgv()[1] == 'help') { + $user = User::getById(DI::userSession()->getLocalUserId()); + + $lang = $user['language']; + + $default_dir = 'addon/advancedcontentfilter/doc/'; + $help_file = 'advancedcontentfilter.md'; + $help_path = $default_dir . $help_file; + if (file_exists($default_dir . $lang . '/' . $help_file)) { + $help_path = $default_dir . $lang . '/' . $help_file; + } + + $content = file_get_contents($help_path); + + $html = Markdown::convert($content, false); + + $html = str_replace('code>', 'key>', $html); + + return $html; + } else { + $t = Renderer::getMarkupTemplate('settings.tpl', 'addon/advancedcontentfilter/'); + return Renderer::replaceMacros($t, [ + '$messages' => [ + 'backtosettings' => DI::l10n()->t('Back to Addon Settings'), + 'title' => DI::l10n()->t('Advanced Content Filter'), + 'add_a_rule' => DI::l10n()->t('Add a Rule'), + 'help' => DI::l10n()->t('Help'), + 'intro' => DI::l10n()->t('Add and manage your personal content filter rules in this screen. Rules have a name and an arbitrary expression that will be matched against post data. For a complete reference of the available operations and variables, check the help page.'), + 'your_rules' => DI::l10n()->t('Your rules'), + 'no_rules' => DI::l10n()->t('You have no rules yet! Start adding one by clicking on the button above next to the title.'), + 'disabled' => DI::l10n()->t('Disabled'), + 'enabled' => DI::l10n()->t('Enabled'), + 'disable_this_rule' => DI::l10n()->t('Disable this rule'), + 'enable_this_rule' => DI::l10n()->t('Enable this rule'), + 'edit_this_rule' => DI::l10n()->t('Edit this rule'), + 'edit_the_rule' => DI::l10n()->t('Edit the rule'), + 'save_this_rule' => DI::l10n()->t('Save this rule'), + 'delete_this_rule' => DI::l10n()->t('Delete this rule'), + 'rule' => DI::l10n()->t('Rule'), + 'close' => DI::l10n()->t('Close'), + 'addtitle' => DI::l10n()->t('Add new rule'), + 'rule_name' => DI::l10n()->t('Rule Name'), + 'rule_expression' => DI::l10n()->t('Rule Expression'), + 'cancel' => DI::l10n()->t('Cancel'), + ], + '$current_theme' => DI::appHelper()->getCurrentTheme(), + '$rules' => DBA::toArray(DBA::select('advancedcontentfilter_rules', [], ['uid' => DI::userSession()->getLocalUserId()])), + '$form_security_token' => BaseModule::getFormSecurityToken() + ]); + } +} + +/* + * Common functions + */ +function advancedcontentfilter_build_fields($data) +{ + $fields = []; + + if (!empty($data['name'])) { + $fields['name'] = $data['name']; + } + + if (!empty($data['expression'])) { + // Using a dummy item to validate the field existence + $condition = ["(`uid` = ? OR `uid` = 0)", DI::userSession()->getLocalUserId()]; + $params = ['order' => ['uid' => true]]; + $item_row = Post::selectFirstForUser(DI::userSession()->getLocalUserId(), [], $condition, $params); + + if (!DBA::isResult($item_row)) { + throw new HTTPException\NotFoundException(DI::l10n()->t('This addon requires this node having at least one post')); + } + + $expressionLanguage = new ExpressionLanguage\ExpressionLanguage(); + $parsedExpression = $expressionLanguage->parse( + $data['expression'], + array_keys(advancedcontentfilter_get_filter_fields(advancedcontentfilter_prepare_item_row($item_row))) + ); + + $serialized = serialize($parsedExpression->getNodes()); + + $fields['expression'] = $data['expression']; + $fields['serialized'] = $serialized; + } + + if (isset($data['active'])) { + $fields['active'] = intval($data['active']); + } else { + $fields['active'] = 1; + } + + return $fields; +} + +/* + * API + */ + +function advancedcontentfilter_get_rules(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + $rules = DBA::toArray(DBA::select('advancedcontentfilter_rules', [], ['uid' => DI::userSession()->getLocalUserId()])); + + $response->getBody()->write(json_encode($rules)); + return $response->withHeader('Content-Type', 'application/json'); +} + +function advancedcontentfilter_get_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + $rule = DBA::selectFirst('advancedcontentfilter_rules', [], ['id' => $args['id'], 'uid' => DI::userSession()->getLocalUserId()]); + + $response->getBody()->write(json_encode($rule)); + return $response->withHeader('Content-Type', 'application/json'); +} + +function advancedcontentfilter_post_rules(ServerRequestInterface $request, ResponseInterface $response) +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + if (!BaseModule::checkFormSecurityToken()) { + throw new HTTPException\BadRequestException(DI::l10n()->t('Invalid form security token, please refresh the page.')); + } + + $data = json_decode($request->getBody(), true); + + try { + $fields = advancedcontentfilter_build_fields($data); + } catch (Exception $e) { + throw new HTTPException\BadRequestException($e->getMessage(), $e); + } + + if (empty($fields['name']) || empty($fields['expression'])) { + throw new HTTPException\BadRequestException(DI::l10n()->t('The rule name and expression are required.')); + } + + $fields['uid'] = DI::userSession()->getLocalUserId(); + $fields['created'] = DateTimeFormat::utcNow(); + + if (!DBA::insert('advancedcontentfilter_rules', $fields)) { + throw new HTTPException\ServiceUnavailableException(DBA::errorMessage()); + } + + $rule = DBA::selectFirst('advancedcontentfilter_rules', [], ['id' => DBA::lastInsertId()]); + + DI::cache()->delete('rules_' . DI::userSession()->getLocalUserId()); + + $response->getBody()->write(json_encode(['message' => DI::l10n()->t('Rule successfully added'), 'rule' => $rule])); + return $response->withHeader('Content-Type', 'application/json'); +} + +function advancedcontentfilter_put_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + if (!BaseModule::checkFormSecurityToken()) { + throw new HTTPException\BadRequestException(DI::l10n()->t('Invalid form security token, please refresh the page.')); + } + + if (!DBA::exists('advancedcontentfilter_rules', ['id' => $args['id'], 'uid' => DI::userSession()->getLocalUserId()])) { + throw new HTTPException\NotFoundException(DI::l10n()->t('Rule doesn\'t exist or doesn\'t belong to you.')); + } + + $data = json_decode($request->getBody(), true); + + try { + $fields = advancedcontentfilter_build_fields($data); + } catch (Exception $e) { + throw new HTTPException\BadRequestException($e->getMessage(), $e); + } + + if (!DBA::update('advancedcontentfilter_rules', $fields, ['id' => $args['id']])) { + throw new HTTPException\ServiceUnavailableException(DBA::errorMessage()); + } + + DI::cache()->delete('rules_' . DI::userSession()->getLocalUserId()); + + $response->getBody()->write(json_encode(['message' => DI::l10n()->t('Rule successfully updated')])); + return $response->withHeader('Content-Type', 'application/json'); +} + +function advancedcontentfilter_delete_rules_id(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + if (!BaseModule::checkFormSecurityToken()) { + throw new HTTPException\BadRequestException(DI::l10n()->t('Invalid form security token, please refresh the page.')); + } + + if (!DBA::exists('advancedcontentfilter_rules', ['id' => $args['id'], 'uid' => DI::userSession()->getLocalUserId()])) { + throw new HTTPException\NotFoundException(DI::l10n()->t('Rule doesn\'t exist or doesn\'t belong to you.')); + } + + if (!DBA::delete('advancedcontentfilter_rules', ['id' => $args['id']])) { + throw new HTTPException\ServiceUnavailableException(DBA::errorMessage()); + } + + DI::cache()->delete('rules_' . DI::userSession()->getLocalUserId()); + + $response->getBody()->write(json_encode(['message' => DI::l10n()->t('Rule successfully deleted')])); + return $response->withHeader('Content-Type', 'application/json'); +} + +function advancedcontentfilter_get_variables_guid(ServerRequestInterface $request, ResponseInterface $response, $args) +{ + if (!DI::userSession()->getLocalUserId()) { + throw new HTTPException\UnauthorizedException(DI::l10n()->t('You must be logged in to use this method')); + } + + if (!isset($args['guid'])) { + throw new HTTPException\BadRequestException(DI::l10n()->t('Missing argument: guid.')); + } + + $condition = ["`guid` = ? AND (`uid` = ? OR `uid` = 0)", $args['guid'], DI::userSession()->getLocalUserId()]; + $params = ['order' => ['uid' => true]]; + $item_row = Post::selectFirstForUser(DI::userSession()->getLocalUserId(), [], $condition, $params); + + if (!DBA::isResult($item_row)) { + throw new HTTPException\NotFoundException(DI::l10n()->t('Unknown post with guid: %s', $args['guid'])); + } + + $return = advancedcontentfilter_get_filter_fields(advancedcontentfilter_prepare_item_row($item_row)); + + $response->getBody()->write(json_encode(['variables' => str_replace('\\\'', '\'', var_export($return, true))])); + return $response->withHeader('Content-Type', 'application/json'); +} + +/** + * This mimimcs the processing performed in Model\Item::prepareBody + * + * @param array $item_row + * @return array + * @throws HTTPException\InternalServerErrorException + * @throws ImagickException + */ +function advancedcontentfilter_prepare_item_row(array $item_row): array +{ + $tags = Tag::populateFromItem($item_row); + + $item_row['tags'] = $tags['tags']; + $item_row['hashtags'] = $tags['hashtags']; + $item_row['mentions'] = $tags['mentions']; + $item_row['attachments'] = DI::postMediaRepository()->splitAttachments($item_row['uri-id']); + + return $item_row; +} diff --git a/advancedcontentfilter/asset/vue/dist/vue.js b/advancedcontentfilter/asset/vue/dist/vue.js new file mode 100644 index 00000000..04bcb375 --- /dev/null +++ b/advancedcontentfilter/asset/vue/dist/vue.js @@ -0,0 +1,11894 @@ +/*! + * Vue.js v2.7.13 + * (c) 2014-2022 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Vue = factory()); +})(this, (function () { 'use strict'; + + var emptyObject = Object.freeze({}); + var isArray = Array.isArray; + // These helpers produce better VM code in JS engines due to their + // explicitness and function inlining. + function isUndef(v) { + return v === undefined || v === null; + } + function isDef(v) { + return v !== undefined && v !== null; + } + function isTrue(v) { + return v === true; + } + function isFalse(v) { + return v === false; + } + /** + * Check if value is primitive. + */ + function isPrimitive(value) { + return (typeof value === 'string' || + typeof value === 'number' || + // $flow-disable-line + typeof value === 'symbol' || + typeof value === 'boolean'); + } + function isFunction(value) { + return typeof value === 'function'; + } + /** + * Quick object check - this is primarily used to tell + * objects from primitive values when we know the value + * is a JSON-compliant type. + */ + function isObject(obj) { + return obj !== null && typeof obj === 'object'; + } + /** + * Get the raw type string of a value, e.g., [object Object]. + */ + var _toString = Object.prototype.toString; + function toRawType(value) { + return _toString.call(value).slice(8, -1); + } + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + */ + function isPlainObject(obj) { + return _toString.call(obj) === '[object Object]'; + } + function isRegExp(v) { + return _toString.call(v) === '[object RegExp]'; + } + /** + * Check if val is a valid array index. + */ + function isValidArrayIndex(val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val); + } + function isPromise(val) { + return (isDef(val) && + typeof val.then === 'function' && + typeof val.catch === 'function'); + } + /** + * Convert a value to a string that is actually rendered. + */ + function toString(val) { + return val == null + ? '' + : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) + ? JSON.stringify(val, null, 2) + : String(val); + } + /** + * Convert an input value to a number for persistence. + * If the conversion fails, return original string. + */ + function toNumber(val) { + var n = parseFloat(val); + return isNaN(n) ? val : n; + } + /** + * Make a map and return a function for checking if a key + * is in that map. + */ + function makeMap(str, expectsLowerCase) { + var map = Object.create(null); + var list = str.split(','); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } : function (val) { return map[val]; }; + } + /** + * Check if a tag is a built-in tag. + */ + var isBuiltInTag = makeMap('slot,component', true); + /** + * Check if an attribute is a reserved attribute. + */ + var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is'); + /** + * Remove an item from an array. + */ + function remove$2(arr, item) { + var len = arr.length; + if (len) { + // fast path for the only / last item + if (item === arr[len - 1]) { + arr.length = len - 1; + return; + } + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1); + } + } + } + /** + * Check whether an object has the property. + */ + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn(obj, key) { + return hasOwnProperty.call(obj, key); + } + /** + * Create a cached version of a pure function. + */ + function cached(fn) { + var cache = Object.create(null); + return function cachedFn(str) { + var hit = cache[str]; + return hit || (cache[str] = fn(str)); + }; + } + /** + * Camelize a hyphen-delimited string. + */ + var camelizeRE = /-(\w)/g; + var camelize = cached(function (str) { + return str.replace(camelizeRE, function (_, c) { return (c ? c.toUpperCase() : ''); }); + }); + /** + * Capitalize a string. + */ + var capitalize = cached(function (str) { + return str.charAt(0).toUpperCase() + str.slice(1); + }); + /** + * Hyphenate a camelCase string. + */ + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function (str) { + return str.replace(hyphenateRE, '-$1').toLowerCase(); + }); + /** + * Simple bind polyfill for environments that do not support it, + * e.g., PhantomJS 1.x. Technically, we don't need this anymore + * since native bind is now performant enough in most browsers. + * But removing it would mean breaking code that was able to run in + * PhantomJS 1.x, so this must be kept for backward compatibility. + */ + /* istanbul ignore next */ + function polyfillBind(fn, ctx) { + function boundFn(a) { + var l = arguments.length; + return l + ? l > 1 + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) + : fn.call(ctx); + } + boundFn._length = fn.length; + return boundFn; + } + function nativeBind(fn, ctx) { + return fn.bind(ctx); + } + // @ts-expect-error bind cannot be `undefined` + var bind$1 = Function.prototype.bind ? nativeBind : polyfillBind; + /** + * Convert an Array-like object to a real Array. + */ + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + /** + * Mix properties into target object. + */ + function extend(to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to; + } + /** + * Merge an Array of Objects into a single Object. + */ + function toObject(arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res; + } + /* eslint-disable no-unused-vars */ + /** + * Perform no operation. + * Stubbing args to make Flow happy without leaving useless transpiled code + * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). + */ + function noop(a, b, c) { } + /** + * Always return false. + */ + var no = function (a, b, c) { return false; }; + /* eslint-enable no-unused-vars */ + /** + * Return the same value. + */ + var identity = function (_) { return _; }; + /** + * Generate a string containing static keys from compiler modules. + */ + function genStaticKeys$1(modules) { + return modules + .reduce(function (keys, m) { + return keys.concat(m.staticKeys || []); + }, []) + .join(','); + } + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + */ + function looseEqual(a, b) { + if (a === b) + return true; + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return (a.length === b.length && + a.every(function (e, i) { + return looseEqual(e, b[i]); + })); + } + else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return (keysA.length === keysB.length && + keysA.every(function (key) { + return looseEqual(a[key], b[key]); + })); + } + else { + /* istanbul ignore next */ + return false; + } + } + catch (e) { + /* istanbul ignore next */ + return false; + } + } + else if (!isObjectA && !isObjectB) { + return String(a) === String(b); + } + else { + return false; + } + } + /** + * Return the first index at which a loosely equal value can be + * found in the array (if value is a plain object, the array must + * contain an object of the same shape), or -1 if it is not present. + */ + function looseIndexOf(arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) + return i; + } + return -1; + } + /** + * Ensure a function is called only once. + */ + function once(fn) { + var called = false; + return function () { + if (!called) { + called = true; + fn.apply(this, arguments); + } + }; + } + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill + function hasChanged(x, y) { + if (x === y) { + return x === 0 && 1 / x !== 1 / y; + } + else { + return x === x || y === y; + } + } + + var SSR_ATTR = 'data-server-rendered'; + var ASSET_TYPES = ['component', 'directive', 'filter']; + var LIFECYCLE_HOOKS = [ + 'beforeCreate', + 'created', + 'beforeMount', + 'mounted', + 'beforeUpdate', + 'updated', + 'beforeDestroy', + 'destroyed', + 'activated', + 'deactivated', + 'errorCaptured', + 'serverPrefetch', + 'renderTracked', + 'renderTriggered' + ]; + + var config = { + /** + * Option merge strategies (used in core/util/options) + */ + // $flow-disable-line + optionMergeStrategies: Object.create(null), + /** + * Whether to suppress warnings. + */ + silent: false, + /** + * Show production mode tip message on boot? + */ + productionTip: true, + /** + * Whether to enable devtools + */ + devtools: true, + /** + * Whether to record perf + */ + performance: false, + /** + * Error handler for watcher errors + */ + errorHandler: null, + /** + * Warn handler for watcher warns + */ + warnHandler: null, + /** + * Ignore certain custom elements + */ + ignoredElements: [], + /** + * Custom user key aliases for v-on + */ + // $flow-disable-line + keyCodes: Object.create(null), + /** + * Check if a tag is reserved so that it cannot be registered as a + * component. This is platform-dependent and may be overwritten. + */ + isReservedTag: no, + /** + * Check if an attribute is reserved so that it cannot be used as a component + * prop. This is platform-dependent and may be overwritten. + */ + isReservedAttr: no, + /** + * Check if a tag is an unknown element. + * Platform-dependent. + */ + isUnknownElement: no, + /** + * Get the namespace of an element + */ + getTagNamespace: noop, + /** + * Parse the real tag name for the specific platform. + */ + parsePlatformTagName: identity, + /** + * Check if an attribute must be bound using property, e.g. value + * Platform-dependent. + */ + mustUseProp: no, + /** + * Perform updates asynchronously. Intended to be used by Vue Test Utils + * This will significantly reduce performance if set to false. + */ + async: true, + /** + * Exposed for legacy reasons + */ + _lifecycleHooks: LIFECYCLE_HOOKS + }; + + /** + * unicode letters used for parsing html tags, component names and property paths. + * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname + * skipping \u10000-\uEFFFF due to it freezing up PhantomJS + */ + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + /** + * Check if a string starts with $ or _ + */ + function isReserved(str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5f; + } + /** + * Define a property. + */ + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + /** + * Parse simple path. + */ + var bailRE = new RegExp("[^".concat(unicodeRegExp.source, ".$_\\d]")); + function parsePath(path) { + if (bailRE.test(path)) { + return; + } + var segments = path.split('.'); + return function (obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) + return; + obj = obj[segments[i]]; + } + return obj; + }; + } + + // can we use __proto__? + var hasProto = '__proto__' in {}; + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined'; + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isEdge = UA && UA.indexOf('edge/') > 0; + UA && UA.indexOf('android') > 0; + var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA); + UA && /chrome\/\d+/.test(UA) && !isEdge; + UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + // Firefox has a "watch" function on Object.prototype... + // @ts-expect-error firebox support + var nativeWatch = {}.watch; + var supportsPassive = false; + if (inBrowser) { + try { + var opts = {}; + Object.defineProperty(opts, 'passive', { + get: function () { + /* istanbul ignore next */ + supportsPassive = true; + } + }); // https://github.com/facebook/flow/issues/285 + window.addEventListener('test-passive', null, opts); + } + catch (e) { } + } + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV + var _isServer; + var isServerRendering = function () { + if (_isServer === undefined) { + /* istanbul ignore if */ + if (!inBrowser && typeof global !== 'undefined') { + // detect presence of vue-server-renderer and avoid + // Webpack shimming the process + _isServer = + global['process'] && global['process'].env.VUE_ENV === 'server'; + } + else { + _isServer = false; + } + } + return _isServer; + }; + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + /* istanbul ignore next */ + function isNative(Ctor) { + return typeof Ctor === 'function' && /native code/.test(Ctor.toString()); + } + var hasSymbol = typeof Symbol !== 'undefined' && + isNative(Symbol) && + typeof Reflect !== 'undefined' && + isNative(Reflect.ownKeys); + var _Set; // $flow-disable-line + /* istanbul ignore if */ if (typeof Set !== 'undefined' && isNative(Set)) { + // use native Set when available. + _Set = Set; + } + else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = /** @class */ (function () { + function Set() { + this.set = Object.create(null); + } + Set.prototype.has = function (key) { + return this.set[key] === true; + }; + Set.prototype.add = function (key) { + this.set[key] = true; + }; + Set.prototype.clear = function () { + this.set = Object.create(null); + }; + return Set; + }()); + } + + var currentInstance = null; + /** + * This is exposed for compatibility with v3 (e.g. some functions in VueUse + * relies on it). Do not use this internally, just use `currentInstance`. + * + * @internal this function needs manual type declaration because it relies + * on previously manually authored types from Vue 2 + */ + function getCurrentInstance() { + return currentInstance && { proxy: currentInstance }; + } + /** + * @internal + */ + function setCurrentInstance(vm) { + if (vm === void 0) { vm = null; } + if (!vm) + currentInstance && currentInstance._scope.off(); + currentInstance = vm; + vm && vm._scope.on(); + } + + /** + * @internal + */ + var VNode = /** @class */ (function () { + function VNode(tag, data, children, text, elm, context, componentOptions, asyncFactory) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text; + this.elm = elm; + this.ns = undefined; + this.context = context; + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = undefined; + this.parent = undefined; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + } + Object.defineProperty(VNode.prototype, "child", { + // DEPRECATED: alias for componentInstance for backwards compat. + /* istanbul ignore next */ + get: function () { + return this.componentInstance; + }, + enumerable: false, + configurable: true + }); + return VNode; + }()); + var createEmptyVNode = function (text) { + if (text === void 0) { text = ''; } + var node = new VNode(); + node.text = text; + node.isComment = true; + return node; + }; + function createTextVNode(val) { + return new VNode(undefined, undefined, undefined, String(val)); + } + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + function cloneVNode(vnode) { + var cloned = new VNode(vnode.tag, vnode.data, + // #7975 + // clone children array to avoid mutating original in case of cloning + // a child. + vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned; + } + + /* not type checking this file because flow doesn't play well with Proxy */ + var initProxy; + { + var allowedGlobals_1 = makeMap('Infinity,undefined,NaN,isFinite,isNaN,' + + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,' + + 'require' // for Webpack/Browserify + ); + var warnNonPresent_1 = function (target, key) { + warn$2("Property or method \"".concat(key, "\" is not defined on the instance but ") + + 'referenced during render. Make sure that this property is reactive, ' + + 'either in the data option, or for class-based components, by ' + + 'initializing the property. ' + + 'See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target); + }; + var warnReservedPrefix_1 = function (target, key) { + warn$2("Property \"".concat(key, "\" must be accessed with \"$data.").concat(key, "\" because ") + + 'properties starting with "$" or "_" are not proxied in the Vue instance to ' + + 'prevent conflicts with Vue internals. ' + + 'See: https://v2.vuejs.org/v2/api/#data', target); + }; + var hasProxy_1 = typeof Proxy !== 'undefined' && isNative(Proxy); + if (hasProxy_1) { + var isBuiltInModifier_1 = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact'); + config.keyCodes = new Proxy(config.keyCodes, { + set: function (target, key, value) { + if (isBuiltInModifier_1(key)) { + warn$2("Avoid overwriting built-in modifier in config.keyCodes: .".concat(key)); + return false; + } + else { + target[key] = value; + return true; + } + } + }); + } + var hasHandler_1 = { + has: function (target, key) { + var has = key in target; + var isAllowed = allowedGlobals_1(key) || + (typeof key === 'string' && + key.charAt(0) === '_' && + !(key in target.$data)); + if (!has && !isAllowed) { + if (key in target.$data) + warnReservedPrefix_1(target, key); + else + warnNonPresent_1(target, key); + } + return has || !isAllowed; + } + }; + var getHandler_1 = { + get: function (target, key) { + if (typeof key === 'string' && !(key in target)) { + if (key in target.$data) + warnReservedPrefix_1(target, key); + else + warnNonPresent_1(target, key); + } + return target[key]; + } + }; + initProxy = function initProxy(vm) { + if (hasProxy_1) { + // determine which proxy handler to use + var options = vm.$options; + var handlers = options.render && options.render._withStripped ? getHandler_1 : hasHandler_1; + vm._renderProxy = new Proxy(vm, handlers); + } + else { + vm._renderProxy = vm; + } + }; + } + + /****************************************************************************** + Copyright (c) Microsoft Corporation. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + var uid$2 = 0; + var pendingCleanupDeps = []; + var cleanupDeps = function () { + for (var i = 0; i < pendingCleanupDeps.length; i++) { + var dep = pendingCleanupDeps[i]; + dep.subs = dep.subs.filter(function (s) { return s; }); + dep._pending = false; + } + pendingCleanupDeps.length = 0; + }; + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + * @internal + */ + var Dep = /** @class */ (function () { + function Dep() { + // pending subs cleanup + this._pending = false; + this.id = uid$2++; + this.subs = []; + } + Dep.prototype.addSub = function (sub) { + this.subs.push(sub); + }; + Dep.prototype.removeSub = function (sub) { + // #12696 deps with massive amount of subscribers are extremely slow to + // clean up in Chromium + // to workaround this, we unset the sub for now, and clear them on + // next scheduler flush. + this.subs[this.subs.indexOf(sub)] = null; + if (!this._pending) { + this._pending = true; + pendingCleanupDeps.push(this); + } + }; + Dep.prototype.depend = function (info) { + if (Dep.target) { + Dep.target.addDep(this); + if (info && Dep.target.onTrack) { + Dep.target.onTrack(__assign({ effect: Dep.target }, info)); + } + } + }; + Dep.prototype.notify = function (info) { + // stabilize the subscriber list first + var subs = this.subs.filter(function (s) { return s; }); + if (!config.async) { + // subs aren't sorted in scheduler if not running async + // we need to sort them now to make sure they fire in correct + // order + subs.sort(function (a, b) { return a.id - b.id; }); + } + for (var i = 0, l = subs.length; i < l; i++) { + var sub = subs[i]; + if (info) { + sub.onTrigger && + sub.onTrigger(__assign({ effect: subs[i] }, info)); + } + sub.update(); + } + }; + return Dep; + }()); + // The current target watcher being evaluated. + // This is globally unique because only one watcher + // can be evaluated at a time. + Dep.target = null; + var targetStack = []; + function pushTarget(target) { + targetStack.push(target); + Dep.target = target; + } + function popTarget() { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + + /* + * not type checking this file because flow doesn't play well with + * dynamically accessing methods on Array prototype + */ + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + var methodsToPatch = [ + 'push', + 'pop', + 'shift', + 'unshift', + 'splice', + 'sort', + 'reverse' + ]; + /** + * Intercept mutating methods and emit events + */ + methodsToPatch.forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + case 'unshift': + inserted = args; + break; + case 'splice': + inserted = args.slice(2); + break; + } + if (inserted) + ob.observeArray(inserted); + // notify change + { + ob.dep.notify({ + type: "array mutation" /* TriggerOpTypes.ARRAY_MUTATION */, + target: this, + key: method + }); + } + return result; + }); + }); + + var rawMap = new WeakMap(); + function reactive(target) { + makeReactive(target, false); + return target; + } + /** + * Return a shallowly-reactive copy of the original object, where only the root + * level properties are reactive. It also does not auto-unwrap refs (even at the + * root level). + */ + function shallowReactive(target) { + makeReactive(target, true); + def(target, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, true); + return target; + } + function makeReactive(target, shallow) { + // if trying to observe a readonly proxy, return the readonly version. + if (!isReadonly(target)) { + { + if (isArray(target)) { + warn$2("Avoid using Array as root value for ".concat(shallow ? "shallowReactive()" : "reactive()", " as it cannot be tracked in watch() or watchEffect(). Use ").concat(shallow ? "shallowRef()" : "ref()", " instead. This is a Vue-2-only limitation.")); + } + var existingOb = target && target.__ob__; + if (existingOb && existingOb.shallow !== shallow) { + warn$2("Target is already a ".concat(existingOb.shallow ? "" : "non-", "shallow reactive object, and cannot be converted to ").concat(shallow ? "" : "non-", "shallow.")); + } + } + var ob = observe(target, shallow, isServerRendering() /* ssr mock reactivity */); + if (!ob) { + if (target == null || isPrimitive(target)) { + warn$2("value cannot be made reactive: ".concat(String(target))); + } + if (isCollectionType(target)) { + warn$2("Vue 2 does not support reactive collection types such as Map or Set."); + } + } + } + } + function isReactive(value) { + if (isReadonly(value)) { + return isReactive(value["__v_raw" /* ReactiveFlags.RAW */]); + } + return !!(value && value.__ob__); + } + function isShallow(value) { + return !!(value && value.__v_isShallow); + } + function isReadonly(value) { + return !!(value && value.__v_isReadonly); + } + function isProxy(value) { + return isReactive(value) || isReadonly(value); + } + function toRaw(observed) { + var raw = observed && observed["__v_raw" /* ReactiveFlags.RAW */]; + return raw ? toRaw(raw) : observed; + } + function markRaw(value) { + if (isObject(value)) { + rawMap.set(value, true); + } + return value; + } + /** + * @internal + */ + function isCollectionType(value) { + var type = toRawType(value); + return (type === 'Map' || type === 'WeakMap' || type === 'Set' || type === 'WeakSet'); + } + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + var NO_INIITIAL_VALUE = {}; + /** + * In some cases we may want to disable observation inside a component's + * update computation. + */ + var shouldObserve = true; + function toggleObserving(value) { + shouldObserve = value; + } + // ssr mock dep + var mockDep = { + notify: noop, + depend: noop, + addSub: noop, + removeSub: noop + }; + /** + * Observer class that is attached to each observed + * object. Once attached, the observer converts the target + * object's property keys into getter/setters that + * collect dependencies and dispatch updates. + */ + var Observer = /** @class */ (function () { + function Observer(value, shallow, mock) { + if (shallow === void 0) { shallow = false; } + if (mock === void 0) { mock = false; } + this.value = value; + this.shallow = shallow; + this.mock = mock; + // this.value = value + this.dep = mock ? mockDep : new Dep(); + this.vmCount = 0; + def(value, '__ob__', this); + if (isArray(value)) { + if (!mock) { + if (hasProto) { + value.__proto__ = arrayMethods; + /* eslint-enable no-proto */ + } + else { + for (var i = 0, l = arrayKeys.length; i < l; i++) { + var key = arrayKeys[i]; + def(value, key, arrayMethods[key]); + } + } + } + if (!shallow) { + this.observeArray(value); + } + } + else { + /** + * Walk through all properties and convert them into + * getter/setters. This method should only be called when + * value type is Object. + */ + var keys = Object.keys(value); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock); + } + } + } + /** + * Observe a list of Array items. + */ + Observer.prototype.observeArray = function (value) { + for (var i = 0, l = value.length; i < l; i++) { + observe(value[i], false, this.mock); + } + }; + return Observer; + }()); + // helpers + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + */ + function observe(value, shallow, ssrMockReactivity) { + if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + return value.__ob__; + } + if (shouldObserve && + (ssrMockReactivity || !isServerRendering()) && + (isArray(value) || isPlainObject(value)) && + Object.isExtensible(value) && + !value.__v_skip /* ReactiveFlags.SKIP */ && + !rawMap.has(value) && + !isRef(value) && + !(value instanceof VNode)) { + return new Observer(value, shallow, ssrMockReactivity); + } + } + /** + * Define a reactive property on an Object. + */ + function defineReactive(obj, key, val, customSetter, shallow, mock) { + var dep = new Dep(); + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && + (val === NO_INIITIAL_VALUE || arguments.length === 2)) { + val = obj[key]; + } + var childOb = !shallow && observe(val, false, mock); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + { + dep.depend({ + target: obj, + type: "get" /* TrackOpTypes.GET */, + key: key + }); + } + if (childOb) { + childOb.dep.depend(); + if (isArray(value)) { + dependArray(value); + } + } + } + return isRef(value) && !shallow ? value.value : value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (!hasChanged(value, newVal)) { + return; + } + if (customSetter) { + customSetter(); + } + if (setter) { + setter.call(obj, newVal); + } + else if (getter) { + // #7981: for accessor properties without setter + return; + } + else if (!shallow && isRef(value) && !isRef(newVal)) { + value.value = newVal; + return; + } + else { + val = newVal; + } + childOb = !shallow && observe(newVal, false, mock); + { + dep.notify({ + type: "set" /* TriggerOpTypes.SET */, + target: obj, + key: key, + newValue: newVal, + oldValue: value + }); + } + } + }); + return dep; + } + function set(target, key, val) { + if ((isUndef(target) || isPrimitive(target))) { + warn$2("Cannot set reactive property on undefined, null, or primitive value: ".concat(target)); + } + if (isReadonly(target)) { + warn$2("Set operation on key \"".concat(key, "\" failed: target is readonly.")); + return; + } + var ob = target.__ob__; + if (isArray(target) && isValidArrayIndex(key)) { + target.length = Math.max(target.length, key); + target.splice(key, 1, val); + // when mocking for SSR, array methods are not hijacked + if (ob && !ob.shallow && ob.mock) { + observe(val, false, true); + } + return val; + } + if (key in target && !(key in Object.prototype)) { + target[key] = val; + return val; + } + if (target._isVue || (ob && ob.vmCount)) { + warn$2('Avoid adding reactive properties to a Vue instance or its root $data ' + + 'at runtime - declare it upfront in the data option.'); + return val; + } + if (!ob) { + target[key] = val; + return val; + } + defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock); + { + ob.dep.notify({ + type: "add" /* TriggerOpTypes.ADD */, + target: target, + key: key, + newValue: val, + oldValue: undefined + }); + } + return val; + } + function del(target, key) { + if ((isUndef(target) || isPrimitive(target))) { + warn$2("Cannot delete reactive property on undefined, null, or primitive value: ".concat(target)); + } + if (isArray(target) && isValidArrayIndex(key)) { + target.splice(key, 1); + return; + } + var ob = target.__ob__; + if (target._isVue || (ob && ob.vmCount)) { + warn$2('Avoid deleting properties on a Vue instance or its root $data ' + + '- just set it to null.'); + return; + } + if (isReadonly(target)) { + warn$2("Delete operation on key \"".concat(key, "\" failed: target is readonly.")); + return; + } + if (!hasOwn(target, key)) { + return; + } + delete target[key]; + if (!ob) { + return; + } + { + ob.dep.notify({ + type: "delete" /* TriggerOpTypes.DELETE */, + target: target, + key: key + }); + } + } + /** + * Collect dependencies on array elements when the array is touched, since + * we cannot intercept array element access like property getters. + */ + function dependArray(value) { + for (var e = void 0, i = 0, l = value.length; i < l; i++) { + e = value[i]; + if (e && e.__ob__) { + e.__ob__.dep.depend(); + } + if (isArray(e)) { + dependArray(e); + } + } + } + + /** + * @internal + */ + var RefFlag = "__v_isRef"; + function isRef(r) { + return !!(r && r.__v_isRef === true); + } + function ref$1(value) { + return createRef(value, false); + } + function shallowRef(value) { + return createRef(value, true); + } + function createRef(rawValue, shallow) { + if (isRef(rawValue)) { + return rawValue; + } + var ref = {}; + def(ref, RefFlag, true); + def(ref, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, shallow); + def(ref, 'dep', defineReactive(ref, 'value', rawValue, null, shallow, isServerRendering())); + return ref; + } + function triggerRef(ref) { + if (!ref.dep) { + warn$2("received object is not a triggerable ref."); + } + { + ref.dep && + ref.dep.notify({ + type: "set" /* TriggerOpTypes.SET */, + target: ref, + key: 'value' + }); + } + } + function unref(ref) { + return isRef(ref) ? ref.value : ref; + } + function proxyRefs(objectWithRefs) { + if (isReactive(objectWithRefs)) { + return objectWithRefs; + } + var proxy = {}; + var keys = Object.keys(objectWithRefs); + for (var i = 0; i < keys.length; i++) { + proxyWithRefUnwrap(proxy, objectWithRefs, keys[i]); + } + return proxy; + } + function proxyWithRefUnwrap(target, source, key) { + Object.defineProperty(target, key, { + enumerable: true, + configurable: true, + get: function () { + var val = source[key]; + if (isRef(val)) { + return val.value; + } + else { + var ob = val && val.__ob__; + if (ob) + ob.dep.depend(); + return val; + } + }, + set: function (value) { + var oldValue = source[key]; + if (isRef(oldValue) && !isRef(value)) { + oldValue.value = value; + } + else { + source[key] = value; + } + } + }); + } + function customRef(factory) { + var dep = new Dep(); + var _a = factory(function () { + { + dep.depend({ + target: ref, + type: "get" /* TrackOpTypes.GET */, + key: 'value' + }); + } + }, function () { + { + dep.notify({ + target: ref, + type: "set" /* TriggerOpTypes.SET */, + key: 'value' + }); + } + }), get = _a.get, set = _a.set; + var ref = { + get value() { + return get(); + }, + set value(newVal) { + set(newVal); + } + }; + def(ref, RefFlag, true); + return ref; + } + function toRefs(object) { + if (!isReactive(object)) { + warn$2("toRefs() expects a reactive object but received a plain one."); + } + var ret = isArray(object) ? new Array(object.length) : {}; + for (var key in object) { + ret[key] = toRef(object, key); + } + return ret; + } + function toRef(object, key, defaultValue) { + var val = object[key]; + if (isRef(val)) { + return val; + } + var ref = { + get value() { + var val = object[key]; + return val === undefined ? defaultValue : val; + }, + set value(newVal) { + object[key] = newVal; + } + }; + def(ref, RefFlag, true); + return ref; + } + + var rawToReadonlyMap = new WeakMap(); + var rawToShallowReadonlyMap = new WeakMap(); + function readonly(target) { + return createReadonly(target, false); + } + function createReadonly(target, shallow) { + if (!isPlainObject(target)) { + { + if (isArray(target)) { + warn$2("Vue 2 does not support readonly arrays."); + } + else if (isCollectionType(target)) { + warn$2("Vue 2 does not support readonly collection types such as Map or Set."); + } + else { + warn$2("value cannot be made readonly: ".concat(typeof target)); + } + } + return target; + } + // already a readonly object + if (isReadonly(target)) { + return target; + } + // already has a readonly proxy + var map = shallow ? rawToShallowReadonlyMap : rawToReadonlyMap; + var existingProxy = map.get(target); + if (existingProxy) { + return existingProxy; + } + var proxy = Object.create(Object.getPrototypeOf(target)); + map.set(target, proxy); + def(proxy, "__v_isReadonly" /* ReactiveFlags.IS_READONLY */, true); + def(proxy, "__v_raw" /* ReactiveFlags.RAW */, target); + if (isRef(target)) { + def(proxy, RefFlag, true); + } + if (shallow || isShallow(target)) { + def(proxy, "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */, true); + } + var keys = Object.keys(target); + for (var i = 0; i < keys.length; i++) { + defineReadonlyProperty(proxy, target, keys[i], shallow); + } + return proxy; + } + function defineReadonlyProperty(proxy, target, key, shallow) { + Object.defineProperty(proxy, key, { + enumerable: true, + configurable: true, + get: function () { + var val = target[key]; + return shallow || !isPlainObject(val) ? val : readonly(val); + }, + set: function () { + warn$2("Set operation on key \"".concat(key, "\" failed: target is readonly.")); + } + }); + } + /** + * Returns a reactive-copy of the original object, where only the root level + * properties are readonly, and does NOT unwrap refs nor recursively convert + * returned properties. + * This is used for creating the props proxy object for stateful components. + */ + function shallowReadonly(target) { + return createReadonly(target, true); + } + + function computed(getterOrOptions, debugOptions) { + var getter; + var setter; + var onlyGetter = isFunction(getterOrOptions); + if (onlyGetter) { + getter = getterOrOptions; + setter = function () { + warn$2('Write operation failed: computed value is readonly'); + } + ; + } + else { + getter = getterOrOptions.get; + setter = getterOrOptions.set; + } + var watcher = isServerRendering() + ? null + : new Watcher(currentInstance, getter, noop, { lazy: true }); + if (watcher && debugOptions) { + watcher.onTrack = debugOptions.onTrack; + watcher.onTrigger = debugOptions.onTrigger; + } + var ref = { + // some libs rely on the presence effect for checking computed refs + // from normal refs, but the implementation doesn't matter + effect: watcher, + get value() { + if (watcher) { + if (watcher.dirty) { + watcher.evaluate(); + } + if (Dep.target) { + if (Dep.target.onTrack) { + Dep.target.onTrack({ + effect: Dep.target, + target: ref, + type: "get" /* TrackOpTypes.GET */, + key: 'value' + }); + } + watcher.depend(); + } + return watcher.value; + } + else { + return getter(); + } + }, + set value(newVal) { + setter(newVal); + } + }; + def(ref, RefFlag, true); + def(ref, "__v_isReadonly" /* ReactiveFlags.IS_READONLY */, onlyGetter); + return ref; + } + + var mark; + var measure; + { + var perf_1 = inBrowser && window.performance; + /* istanbul ignore if */ + if (perf_1 && + // @ts-ignore + perf_1.mark && + // @ts-ignore + perf_1.measure && + // @ts-ignore + perf_1.clearMarks && + // @ts-ignore + perf_1.clearMeasures) { + mark = function (tag) { return perf_1.mark(tag); }; + measure = function (name, startTag, endTag) { + perf_1.measure(name, startTag, endTag); + perf_1.clearMarks(startTag); + perf_1.clearMarks(endTag); + // perf.clearMeasures(name) + }; + } + } + + var normalizeEvent = cached(function (name) { + var passive = name.charAt(0) === '&'; + name = passive ? name.slice(1) : name; + var once = name.charAt(0) === '~'; // Prefixed last, checked first + name = once ? name.slice(1) : name; + var capture = name.charAt(0) === '!'; + name = capture ? name.slice(1) : name; + return { + name: name, + once: once, + capture: capture, + passive: passive + }; + }); + function createFnInvoker(fns, vm) { + function invoker() { + var fns = invoker.fns; + if (isArray(fns)) { + var cloned = fns.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments, vm, "v-on handler"); + } + } + else { + // return handler return value for single handlers + return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler"); + } + } + invoker.fns = fns; + return invoker; + } + function updateListeners(on, oldOn, add, remove, createOnceHandler, vm) { + var name, cur, old, event; + for (name in on) { + cur = on[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn$2("Invalid handler for event \"".concat(event.name, "\": got ") + String(cur), vm); + } + else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on[name] = createOnceHandler(event.name, cur, event.capture); + } + add(event.name, cur, event.capture, event.passive, event.params); + } + else if (cur !== old) { + old.fns = cur; + on[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on[name])) { + event = normalizeEvent(name); + remove(event.name, oldOn[name], event.capture); + } + } + } + + function mergeVNodeHook(def, hookKey, hook) { + if (def instanceof VNode) { + def = def.data.hook || (def.data.hook = {}); + } + var invoker; + var oldHook = def[hookKey]; + function wrappedHook() { + hook.apply(this, arguments); + // important: remove merged hook to ensure it's called only once + // and prevent memory leak + remove$2(invoker.fns, wrappedHook); + } + if (isUndef(oldHook)) { + // no existing hook + invoker = createFnInvoker([wrappedHook]); + } + else { + /* istanbul ignore if */ + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + // already a merged invoker + invoker = oldHook; + invoker.fns.push(wrappedHook); + } + else { + // existing plain hook + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + invoker.merged = true; + def[hookKey] = invoker; + } + + function extractPropsFromVNodeData(data, Ctor, tag) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return; + } + var res = {}; + var attrs = data.attrs, props = data.props; + if (isDef(attrs) || isDef(props)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + { + var keyInLowerCase = key.toLowerCase(); + if (key !== keyInLowerCase && attrs && hasOwn(attrs, keyInLowerCase)) { + tip("Prop \"".concat(keyInLowerCase, "\" is passed to component ") + + "".concat(formatComponentName( + // @ts-expect-error tag is string + tag || Ctor), ", but the declared prop name is") + + " \"".concat(key, "\". ") + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"".concat(altKey, "\" instead of \"").concat(key, "\".")); + } + } + checkProp(res, props, key, altKey, true) || + checkProp(res, attrs, key, altKey, false); + } + } + return res; + } + function checkProp(res, hash, key, altKey, preserve) { + if (isDef(hash)) { + if (hasOwn(hash, key)) { + res[key] = hash[key]; + if (!preserve) { + delete hash[key]; + } + return true; + } + else if (hasOwn(hash, altKey)) { + res[key] = hash[altKey]; + if (!preserve) { + delete hash[altKey]; + } + return true; + } + } + return false; + } + + // The template compiler attempts to minimize the need for normalization by + // statically analyzing the template at compile time. + // + // For plain HTML markup, normalization can be completely skipped because the + // generated render function is guaranteed to return Array. There are + // two cases where extra normalization is needed: + // 1. When the children contains components - because a functional component + // may return an Array instead of a single root. In this case, just a simple + // normalization is needed - if any child is an Array, we flatten the whole + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep + // because functional components already normalize their own children. + function simpleNormalizeChildren(children) { + for (var i = 0; i < children.length; i++) { + if (isArray(children[i])) { + return Array.prototype.concat.apply([], children); + } + } + return children; + } + // 2. When the children contains constructs that always generated nested Arrays, + // e.g.