/*

  This application scans for Netscape plugins and create a cache and
  the necessary mime and service files.


  Copyright (c) 2000 Matthias Hoelzer-Kluepfel <hoelzer@kde.org>
                     Stefan Schimanski <1Stein@gmx.de>

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

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

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

*/

#include <mimetypewriter.h>
#include <config-apps.h>

#include <sys/types.h>
#include <sys/wait.h>

#include <errno.h>
#include <signal.h>
#include <unistd.h>

#include <QDir>
#include <QFile>
#include <QTextStream>
#include <QRegExp>
#include <QBuffer>

#include <QtDBus/QtDBus>

#include <kapplication.h>
#include <kdebug.h>
#include <kglobal.h>
#include <kstandarddirs.h>
#include <klibloader.h>
#include <kconfig.h>
#include <kconfiggroup.h>
#include <kcrash.h>
#include <kdesktopfile.h>
#include <kservicetype.h>
#include <kmimetype.h>
#include <kcmdlineargs.h>
#include <kaboutdata.h>
#include <klocale.h>

#include "sdk/npupp.h"

#include "plugin_paths.h"

static int showProgress=0;

// provide these symbols when compiling with gcc 3.x

#if defined(__GNUC__) && defined(__GNUC_MINOR__)
#define KDE_GNUC_PREREQ(maj,min) \
  ((__GNUC__ << 16) + __GNUC_MINOR__ >= ((maj) << 16) + (min))
#else
#define KDE_GNUC_PREREQ(maj,min) 0
#endif

#if defined(__GNUC__) && KDE_GNUC_PREREQ(3,0)
extern "C" void* __builtin_new(size_t s)
{
   return operator new(s);
}

extern "C" void __builtin_delete(void* p)
{
   operator delete(p);
}

extern "C" void* __builtin_vec_new(size_t s)
{
   return operator new[](s);
}

extern "C" void __builtin_vec_delete(void* p)
{
   operator delete[](p);
}

extern "C" void __pure_virtual()
{
   abort();
}
#endif

KConfig *infoConfig = 0;

static const char s_mimeTypeMarker[] = "MimeType generated by nspluginscan";


static bool isPluginMimeType( const QString &fname )
{
    QFile file(fname);
    if (file.open(QIODevice::ReadOnly)) {
        const QByteArray firstLine = file.readLine();
        if (!firstLine.startsWith("<?xml")) {
            kWarning() << "Malformed XML file:" << fname;
            return false;
        }
        const QByteArray secondLine = file.readLine();
        // In Qt-4.3.3 the newlines are missing around the comment
        // so the comment marker is part of the first line instead of the second one
        // So we grep both.
        return secondLine.startsWith(s_mimeTypeMarker) || firstLine.contains(s_mimeTypeMarker);
    }
    return false;
}


static QStringList deletePluginMimeTypes()
{
    QStringList removedMimes;

    // get local mime type directory
    const QString dirPath = KGlobal::dirs()->saveLocation( "xdgdata-mime", "packages" );
    kDebug(1433) << "Removing nsplugin MIME types in " << dirPath;
    QDir dir( dirPath, QString(), QDir::Name|QDir::IgnoreCase, QDir::Files );
    if ( !dir.exists() ) {
        kDebug(1433) << "Local mime directory not found, nothing to remove";
        return removedMimes;
    }

    // check all user mime types for our own marker
    kDebug(1433) << " - Looking in " << dirPath;
    for (unsigned int i=0; i<dir.count(); i++) {

        // check mimetype file
        const QString file = dir[i];
        kDebug(1433) << "   - Checking" << file;
        if ( isPluginMimeType(dir.absoluteFilePath(file)) ) {
            kDebug(1433) << "     - Removing" << file;
            dir.remove( file );
            QString mimeType = file;
            mimeType.replace('-', '/');
            Q_ASSERT(mimeType.endsWith(".xml"));
            mimeType.truncate(mimeType.length()-4);
            removedMimes.append(mimeType);
            kDebug(1433) << "     - Removing" << file << '(' << mimeType << ')';
        }
    }
    return removedMimes;
}


static void generateMimeType( const QString &mime, const QString &extensions, const QString &pluginName, const QString &description )
{
    kDebug(1433) << "-> generateMimeType mime=" << mime << " ext="<< extensions;

    // get directory from mime string
    int pos = mime.lastIndexOf('/');
    if ( pos<0 ) {
        kDebug(1433) << "Invalid MIME type " << mime;
        kDebug(1433) << "<- generateMimeType";
        return;
    }

    // create mime type definition file
    MimeTypeWriter mimeTypeWriter(mime);
    mimeTypeWriter.setMarker(QString::fromLatin1(s_mimeTypeMarker));
    if (!description.isEmpty()) {
        mimeTypeWriter.setComment(description);
    } else {
        // TODO remove mimeinfo here, after message freeze
        mimeTypeWriter.setComment(i18n("Netscape plugin mimeinfo") + ' ' + pluginName);
    }

    // Maybe we should do it only if the icon named after the mimetype doesn't exist on the system...
    // but this is quite unlikely, why would have the icon and not the mimetype.
    mimeTypeWriter.setIconName("x-kde-nsplugin-generated");

    if (!extensions.isEmpty()) {
        const QStringList exts = extensions.split(',');
        QStringList patterns;
        for (QStringList::const_iterator it=exts.constBegin(); it != exts.constEnd(); ++it)
            patterns.append( "*." + (*it).trimmed() );
        mimeTypeWriter.setPatterns(patterns);
    }

    mimeTypeWriter.write();

    // In KDE 3 we wrote X-KDE-AutoEmbed=true into the mimetype desktop file.
    // In KDE 4 this has moved to a filetypesrc config file.
    KSharedConfig::Ptr fileTypesConfig = KSharedConfig::openConfig("filetypesrc", KConfig::NoGlobals);
    fileTypesConfig->group("EmbedSettings").writeEntry("embed-" + mime, true);

    kDebug(1433) << "<- generateMimeType";
}


void registerPlugin( const QString &name, const QString &description,
                     const QString &file, const QString &mimeInfo )
{
    // global stuff
    KConfigGroup cg( infoConfig, QString() );
    int num = cg.readEntry( "number", 0 );
    cg.writeEntry( "number", num+1 );

    cg = KConfigGroup(infoConfig,QString::number(num));
    // create plugin info
    cg.writeEntry( "name", name );
    cg.writeEntry( "description", description );
    cg.writeEntry( "file", file );
    cg.writeEntry( "mime", mimeInfo );
}

static void segv_handler(int)
{
    _exit(255);
}

static int tryCheck(int write_fd, const QString &absFile)
{
    KLibrary *_handle = KLibLoader::self()->library( QFile::encodeName(absFile) );
    if (!_handle) {
        kDebug(1433) << " - open failed with message " <<
		         KLibLoader::self()->lastErrorMessage() << ", skipping " << endl;
        return 1;
    }

    // ask for name and description
    QString name = i18n("Unnamed plugin");
    QString description;

    NPError (*func_GetValue)(void *, NPPVariable, void *) =
        (NPError(*)(void *, NPPVariable, void *))
        _handle->resolveFunction("NP_GetValue");
    if ( func_GetValue ) {

        // get name
        char *buf = 0;
        NPError err = func_GetValue( 0, NPPVpluginNameString,
                                     (void*)&buf );
        if ( err==NPERR_NO_ERROR )
            name = QString::fromLatin1( buf );
        kDebug() << "name = " << name;

        // get name
        NPError nperr = func_GetValue( 0, NPPVpluginDescriptionString,
                                     (void*)&buf );
        if ( nperr==NPERR_NO_ERROR )
            description = QString::fromLatin1( buf );
        kDebug() << "description = " << description;
    }
    else
        kWarning() << "Plugin doesn't implement NP_GetValue"  ;

    // get mime description function pointer
    char* (*func_GetMIMEDescription)() =
        (char *(*)())_handle->resolveFunction("NP_GetMIMEDescription");
    if ( !func_GetMIMEDescription ) {
        kDebug(1433) << " - no GetMIMEDescription, skipping";
        KLibLoader::self()->unloadLibrary( QFile::encodeName(absFile) );
        return 1;
    }

    // ask for mime information
    QString mimeInfo = func_GetMIMEDescription();
    if ( mimeInfo.isEmpty() ) {
        kDebug(1433) << " - no mime info returned, skipping";
        KLibLoader::self()->unloadLibrary( QFile::encodeName(absFile) );
        return 1;
    }

    // remove version info, as it is not used at the moment
    QRegExp versionRegExp(";version=[^:]*:");
    mimeInfo.replace( versionRegExp, ":");
    if (!mimeInfo.isEmpty() && !mimeInfo.endsWith(';')) {
        mimeInfo += ';'; // XDG compliance
    }

    // unload plugin lib
    kDebug(1433) << " - unloading plugin";
    KLibLoader::self()->unloadLibrary( QFile::encodeName(absFile) );

    // create a QDataStream for our IPC pipe (to send plugin info back to the parent)
    QFile stream_file;
    stream_file.open(write_fd, QIODevice::WriteOnly);
    QDataStream stream(&stream_file);

    // return the gathered info to the parent
    stream << name;
    stream << description;
    stream << mimeInfo;

    return 0;
}

void scanDirectory( const QString &dir, QStringList &mimeInfoList,
                    QTextStream &cache )
{
    kDebug(1433) << "-> scanDirectory dir=" << dir;

    // iterate over all files
    QDir files( dir, QString(), QDir::Name|QDir::IgnoreCase, QDir::Files );
    if ( !files.exists( dir ) ) {
        kDebug(1433) << "No files found";
        kDebug(1433) << "<- scanDirectory dir=" << dir;
        return;
    }

    for (unsigned int i=0; i<files.count(); i++) {
        QString extension;
        int j = files[i].lastIndexOf('.');
        if (j > 0)
           extension = files[i].mid(j+1);

        // ignore crashing libs
        if ( files[i]=="librvplayer.so" ||      // RealPlayer 5
             files[i]=="libnullplugin.so" ||    // Netscape Default Plugin
             files[i]=="cult3dplugin.so" ||     // Cult 3d plugin
             extension == "jar" ||              // Java archive
             extension == "zip" ||              // Zip file (for classes)
             extension == "class" ||            // Java class
             extension == "png" ||              // PNG Image
             extension == "jpg" ||              // JPEG image
             extension == "gif" ||              // GIF image
             extension == "bak" ||              // .so.bak-up files
             extension == "tmp" ||              // tmp files
             extension == "xpt" ||              // XPConnect
             extension.startsWith("htm")        // HTML
            )
            continue;

        // get absolute file path
        QString absFile = files.absoluteFilePath( files[i] );
        kDebug(1433) << "Checking library " << absFile;

        // open the library and ask for the mimetype
        kDebug(1433) << " - opening " << absFile;

        cache.flush();
        // fork, so that a crash in the plugin won't stop the scanning of other plugins
        int pipes[2];
        if (pipe(pipes) != 0) continue;

        int loader_pid = fork();

        if (loader_pid == -1) {
            // unable to fork
            continue;
        } else if (loader_pid == 0) {
           // inside the child
           close(pipes[0]);
           KCrash::setCrashHandler(segv_handler);
           _exit(tryCheck(pipes[1], absFile));
        } else {
           close(pipes[1]);

           QBuffer m_buffer;
           m_buffer.open(QIODevice::WriteOnly);

           QFile q_read_pipe;
           q_read_pipe.open(pipes[0], QIODevice::ReadOnly);

           char *data = (char *)malloc(4096);
           if (!data) continue;
           int size;

           // when the child closes, we'll get an EOF (size == 0)
           while ((size = q_read_pipe.read(data, 4096)) > 0)
               m_buffer.write(data, size);
           free(data);

           q_read_pipe.close();
           close(pipes[0]); // we no longer need the pipe's reading end

           // close the buffer and open for reading (from the start)
           m_buffer.close();
           m_buffer.open(QIODevice::ReadOnly);

           // create a QDataStream for our buffer
           QDataStream stream(&m_buffer);

           if (stream.atEnd()) continue;

           QString name, description, mimeInfo;
           stream >> name;
           stream >> description;
           stream >> mimeInfo;

           bool actuallyUsing = false;

           // get mime types from string
           QStringList types = mimeInfo.split( ';' );
           QStringList::const_iterator type;
           for ( type=types.constBegin(); type!=types.constEnd(); ++type ) {

              kDebug(1433) << " - type=" << *type;
              name = name.replace( ':', "%3A" );

              QString entry = name + ':' + (*type).trimmed();
              if ( !mimeInfoList.contains( entry ) ) {
                  if (!actuallyUsing) {
                      // note the plugin name
                      cache << "[" << absFile << "]" << endl;
                      actuallyUsing = true;
                  }

                  // write into type cache
                  QStringList tokens = (*type).split(':', QString::KeepEmptyParts);
                  QStringList::const_iterator token;
                  token = tokens.constBegin();
                  cache << (*token).toLower();
                  ++token;
                  for ( ; token!=tokens.constEnd(); ++token )
                      cache << ":" << *token;
                  cache << endl;

                  // append type to MIME type list
                  mimeInfoList.append( entry );
              }
           }

           // register plugin for javascript
           registerPlugin( name, description, files[i], mimeInfo );
        }
    }

    // iterate over all sub directories
    // NOTE: Mozilla doesn't iterate over subdirectories of the plugin dir.
    // We still do (as Netscape 4 did).
    QDir dirs( dir, QString(), QDir::Name|QDir::IgnoreCase, QDir::Dirs );
    if ( !dirs.exists() )
      return;

    static int depth = 0; // avoid recursion because of symlink circles
    depth++;
    for ( unsigned int i=0; i<dirs.count(); i++ ) {
        if ( depth<8 && !dirs[i].contains(".") )
            scanDirectory( dirs.absoluteFilePath(dirs[i]), mimeInfoList, cache );
    }
    depth--;

    kDebug() << "<- scanDirectory dir=" << dir;
}


void writeServicesFile( const QStringList &mimeTypes )
{
    QString fname = KGlobal::dirs()->saveLocation("services", "")
                    + "/nsplugin.desktop";
    kDebug(1433) << "Creating services file " << fname;

    QFile f(fname);
    if ( f.open(QIODevice::WriteOnly) ) {

        QTextStream ts(&f);

        ts << "[Desktop Entry]" << endl;
        ts << "Name=" << i18n("Netscape plugin viewer") << endl;
        ts << "Type=Service" << endl;
        ts << "Icon=netscape" << endl;
        ts << "Comment=" << i18n("Netscape plugin viewer") << endl;
        ts << "X-KDE-Library=libnsplugin" << endl;
        ts << "InitialPreference=0" << endl;
        ts << "ServiceTypes=KParts/ReadOnlyPart,Browser/View" << endl;
        ts << "X-KDE-BrowserView-PluginsInfo=nsplugins/pluginsinfo" << endl;

        if (mimeTypes.count() > 0)
            ts << "MimeType=" << mimeTypes.join(";") << ";" << endl;

        f.close();
    } else
        kDebug(1433) << "Failed to open file " << fname;
}


void removeExistingExtensions( QString &extension )
{
    QStringList filtered;
    const QStringList exts = extension.split( ',' );
    for ( QStringList::const_iterator it=exts.constBegin(); it!=exts.constEnd(); ++it ) {
        QString ext = (*it).trimmed();
        if ( ext == "*" ) // some plugins have that, but we don't want to associate a mimetype with *.*!
            continue;

        KMimeType::Ptr mime = KMimeType::findByUrl( KUrl("file:///foo."+ext ),
                                                    0, true, true );
        if( mime->name()=="application/octet-stream" ||
            mime->comment().left(8)=="Netscape" ) {
            kDebug() << "accepted";
            filtered.append( ext );
        }
    }

    extension = filtered.join( "," );
}

void sigChildHandler(int)
{
   // since waitpid and write change errno, we have to save it and restore it
   // (Richard Stevens, Advanced programming in the Unix Environment)
   int saved_errno = errno;

   while (waitpid(-1, 0, WNOHANG) == 0)
   	;

   errno = saved_errno;
}


int main( int argc, char **argv )
{
    KAboutData aboutData( "nspluginscan", "nsplugin", ki18n("nspluginscan"),
                          "0.3", ki18n("nspluginscan"), KAboutData::License_GPL,
                          ki18n("(c) 2000,2001 by Stefan Schimanski") );

    KCmdLineArgs::init( argc, argv, &aboutData );

    KCmdLineOptions options;
    options.add("verbose", ki18n("Show progress output for GUI"));
    KCmdLineArgs::addCmdLineOptions( options );
    KCmdLineArgs *args = KCmdLineArgs::parsedArgs();

    showProgress = args->isSet("verbose");
    if (showProgress) {
      printf("10\n"); fflush(stdout);
    }

    KApplication app(false);

    // Set up SIGCHLD handler
    struct sigaction act;
    act.sa_handler=sigChildHandler;
    sigemptyset(&(act.sa_mask));
    sigaddset(&(act.sa_mask), SIGCHLD);
    // Make sure we don't block this signal. gdb tends to do that :-(
    sigprocmask(SIG_UNBLOCK, &(act.sa_mask), 0);

    act.sa_flags = SA_NOCLDSTOP;

    // CC: take care of SunOS which automatically restarts interrupted system
    // calls (and thus does not have SA_RESTART)

#ifdef SA_RESTART
    act.sa_flags |= SA_RESTART;
#endif

    struct sigaction oldact;
    sigaction( SIGCHLD, &act, &oldact );


    // set up the paths used to look for plugins
    QStringList searchPaths = getSearchPaths();
    QStringList mimeInfoList;

    infoConfig = new KConfig( KGlobal::dirs()->saveLocation("data", "nsplugins") +
                              "/pluginsinfo" );
    infoConfig->group("<default>").writeEntry( "number", 0 );

    // open the cache file for the mime information
    QString cacheName = KGlobal::dirs()->saveLocation("data", "nsplugins")+"/cache";
    kDebug(1433) << "Creating MIME cache file " << cacheName;
    QFile cachef(cacheName);
    if (!cachef.open(QIODevice::WriteOnly))
        return -1;
    QTextStream cache(&cachef);
    if (showProgress) {
      printf("20\n"); fflush(stdout);
    }

    // read in the plugins mime information
    kDebug(1433) << "Scanning directories";
    int count = searchPaths.count();
    int i = 0;
    for ( QStringList::const_iterator it = searchPaths.constBegin();
          it != searchPaths.constEnd(); ++it, ++i)
    {
        if ((*it).isEmpty())
            continue;
        scanDirectory( *it, mimeInfoList, cache );
        if (showProgress) {
          printf("%d\n", 25 + (50*i) / count ); fflush(stdout);
        }
    }

    if (showProgress) {
      printf("75\n"); fflush(stdout);
    }

    // We're done with forking,
    // KProcess needs SIGCHLD to be reset to what it was initially
    sigaction( SIGCHLD, &oldact, 0 );

    // delete old mime types
    kDebug(1433) << "Removing old mimetypes";
    const QStringList oldMimes = deletePluginMimeTypes();
    bool mimeTypesChanged = !oldMimes.isEmpty();

    if (showProgress) {
      printf("80\n");  fflush(stdout);
    }

    // write mimetype files
    kDebug(1433) << "Creating MIME type descriptions";
    QStringList mimeTypes;
    for ( QStringList::const_iterator it=mimeInfoList.constBegin();
          it!=mimeInfoList.constEnd(); ++it) {

      kDebug(1433) << "Handling MIME type " << *it;

      QStringList info = (*it).split(':', QString::KeepEmptyParts);
      if ( info.count()==4 ) {
          QString pluginName = info[0];
          QString type = info[1].toLower();
          QString extension = info[2];
          QString desc = info[3];

          // append to global mime type list
          if ( !mimeTypes.contains(type) ) {
              kDebug(1433) << " - mimeType=" << type;
              mimeTypes.append( type );

              // write or update mime type file, if
              // 1) it doesn't exist in ksycoca (meaning we never heard of it)
              // 2) or we just deleted it [it's still in ksycoca though]
              // This prevents noticing that a shared-mime-info upgrade brought
              // us a mimetype we needed; but doing this right requires launching
              // kbuildsycoca4 after removing mimetypes above, and that's really slow
              bool mustWriteMimeType = KMimeType::mimeType(type).isNull();
              if (!mustWriteMimeType)
                  mustWriteMimeType = oldMimes.contains(type);
              if ( mustWriteMimeType ) {
                  kDebug(1433) << " - creating MIME type description";
                  removeExistingExtensions( extension );
                  generateMimeType( type, extension, pluginName, desc );
                  mimeTypesChanged = true;
              } else {
                  kDebug(1433) << " - already exists";
              }
          }
        }
    }

    // done with new mimetypes, run update-mime-database
    if (mimeTypesChanged) {
        MimeTypeWriter::runUpdateMimeDatabase();
        // note that we'll run kbuildsycoca below anyway
    }

    if (showProgress) {
      printf("85\n"); fflush(stdout);
    }

    // close files
    kDebug(1433) << "Closing cache file";
    cachef.close();

    infoConfig->sync();
    delete infoConfig;

    // write plugin lib service file
    writeServicesFile( mimeTypes );
    if (showProgress) {
      printf("90\n"); fflush(stdout);
    }

    // Tell kded to update sycoca database.
    QDBusInterface kbuildsycoca("org.kde.kded", "/kbuildsycoca",
                                "org.kde.kded");
    if (kbuildsycoca.isValid())
        kbuildsycoca.call("recreate");
}
